mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
add v1 secret scanning
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "unused-imports"],
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"unused-imports"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
@@ -8,14 +11,29 @@
|
||||
],
|
||||
"rules": {
|
||||
"no-console": 2,
|
||||
"quotes": ["error", "double", { "avoidEscape": true }],
|
||||
"comma-dangle": ["error", "only-multiline"],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"only-multiline"
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"sort-imports": ["error", { "ignoreDeclarationSort": true }]
|
||||
"sort-imports": "off"
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import * as signupController from "./signupController";
|
||||
import * as userActionController from "./userActionController";
|
||||
import * as userController from "./userController";
|
||||
import * as workspaceController from "./workspaceController";
|
||||
import * as secretScanningController from "./secretScanningController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
@@ -30,4 +31,5 @@ export {
|
||||
userActionController,
|
||||
userController,
|
||||
workspaceController,
|
||||
secretScanningController
|
||||
};
|
||||
|
||||
89
backend/src/controllers/v1/secretScanningController.ts
Normal file
89
backend/src/controllers/v1/secretScanningController.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Request, Response } from "express";
|
||||
import GitAppInstallationSession from "../../models/gitAppInstallationSession";
|
||||
import crypto from "crypto";
|
||||
import { Types } from "mongoose";
|
||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
||||
import GitAppOrganizationInstallation from "../../models/gitAppOrganizationInstallation";
|
||||
import { MembershipOrg } from "../../models";
|
||||
import GitRisks, { STATUS_UNRESOLVED } from "../../models/gitRisks";
|
||||
|
||||
export const createInstallationSession = async (req: Request, res: Response) => {
|
||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||
await GitAppInstallationSession.findByIdAndUpdate(
|
||||
req.organization,
|
||||
{
|
||||
organization: new Types.ObjectId(req.organization),
|
||||
sessionId: sessionId,
|
||||
user: new Types.ObjectId(req.user._id)
|
||||
},
|
||||
{ upsert: true }
|
||||
).lean();
|
||||
|
||||
res.send({
|
||||
sessionId: sessionId
|
||||
})
|
||||
}
|
||||
|
||||
export const linkInstallationToOrganization = async (req: Request, res: Response) => {
|
||||
const { installationId, sessionId } = req.body
|
||||
|
||||
const installationSession = await GitAppInstallationSession.findOneAndDelete({ sessionId: sessionId })
|
||||
if (!installationSession) {
|
||||
throw UnauthorizedRequestError()
|
||||
}
|
||||
|
||||
const userMembership = await MembershipOrg.find({ user: req.user._id, organization: installationSession.organization })
|
||||
if (!userMembership) {
|
||||
throw UnauthorizedRequestError()
|
||||
}
|
||||
|
||||
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate({
|
||||
organizationId: installationSession.organization,
|
||||
}, {
|
||||
installationId: installationId,
|
||||
organizationId: installationSession.organization,
|
||||
user: installationSession.user
|
||||
}, {
|
||||
upsert: true
|
||||
}).lean()
|
||||
|
||||
res.json(installationLink)
|
||||
}
|
||||
|
||||
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params
|
||||
try {
|
||||
const appInstallation = await GitAppOrganizationInstallation.findOne({ organizationId: organizationId }).lean()
|
||||
if (!appInstallation) {
|
||||
res.json({
|
||||
appInstallationComplete: false
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
appInstallationComplete: true
|
||||
})
|
||||
} catch {
|
||||
res.json({
|
||||
appInstallationComplete: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const getRisksForOrganization = async (req: Request, res: Response) => {
|
||||
const { organizationId } = req.params
|
||||
const risks = await GitRisks.find({ organization: organizationId, status: STATUS_UNRESOLVED }).lean()
|
||||
res.json({
|
||||
risks: risks
|
||||
})
|
||||
}
|
||||
|
||||
export const updateRisksStatus = async (req: Request, res: Response) => {
|
||||
const { riskId } = req.params
|
||||
const { status } = req.body
|
||||
const risks = await GitRisks.findByIdAndUpdate(riskId, {
|
||||
sttaus: status
|
||||
}).lean()
|
||||
|
||||
res.json(risks)
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
organization as v1OrganizationRouter,
|
||||
password as v1PasswordRouter,
|
||||
secret as v1SecretRouter,
|
||||
secretScanning as v1SecretScanningRouter,
|
||||
secretsFolder as v1SecretsFolder,
|
||||
serviceToken as v1ServiceTokenRouter,
|
||||
signup as v1SignupRouter,
|
||||
@@ -42,8 +43,8 @@ import {
|
||||
workspace as v1WorkspaceRouter,
|
||||
} from "./routes/v1";
|
||||
import {
|
||||
signup as v2SignupRouter,
|
||||
auth as v2AuthRouter,
|
||||
signup as v2SignupRouter,
|
||||
users as v2UsersRouter,
|
||||
organizations as v2OrganizationsRouter,
|
||||
workspace as v2WorkspaceRouter,
|
||||
@@ -125,6 +126,7 @@ const main = async () => {
|
||||
app.use("/api/v1/integration", v1IntegrationRouter);
|
||||
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
|
||||
app.use("/api/v1/folders", v1SecretsFolder);
|
||||
app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
|
||||
|
||||
// v2 routes (improvements)
|
||||
app.use("/api/v2/signup", v2SignupRouter);
|
||||
|
||||
34
backend/src/models/gitAppInstallationSession.ts
Normal file
34
backend/src/models/gitAppInstallationSession.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
type GitAppInstallationSession = {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
organization: Types.ObjectId;
|
||||
user: Types.ObjectId;
|
||||
}
|
||||
|
||||
const gitAppInstallationSession = new Schema<GitAppInstallationSession>({
|
||||
id: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
sessionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User"
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const GitAppInstallationSession = model<GitAppInstallationSession>("git_app_installation_session", gitAppInstallationSession);
|
||||
|
||||
export default GitAppInstallationSession;
|
||||
31
backend/src/models/gitAppOrganizationInstallation.ts
Normal file
31
backend/src/models/gitAppOrganizationInstallation.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
|
||||
type Installation = {
|
||||
installationId: string
|
||||
organizationId: string
|
||||
user: Schema.Types.ObjectId
|
||||
};
|
||||
|
||||
|
||||
const gitAppOrganizationInstallation = new Schema<Installation>({
|
||||
installationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
organizationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
|
||||
|
||||
export default GitAppOrganizationInstallation;
|
||||
148
backend/src/models/gitRisks.ts
Normal file
148
backend/src/models/gitRisks.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
|
||||
export const STATUS_RESOLVED_FALSE_POSITIVE = "RESOLVED_FALSE_POSITIVE";
|
||||
export const STATUS_RESOLVED_REVOKED = "RESOLVED_REVOKED";
|
||||
export const STATUS_RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED";
|
||||
export const STATUS_UNRESOLVED = "UNRESOLVED";
|
||||
|
||||
export type GitRisks = {
|
||||
id: string;
|
||||
description: string;
|
||||
startLine: string;
|
||||
endLine: string;
|
||||
startColumn: string;
|
||||
endColumn: string;
|
||||
match: string;
|
||||
secret: string;
|
||||
file: string;
|
||||
symlinkFile: string;
|
||||
commit: string;
|
||||
entropy: string;
|
||||
author: string;
|
||||
email: string;
|
||||
date: string;
|
||||
message: string;
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
|
||||
isFalsePositive: boolean; // New field for marking risks as false positives
|
||||
isResolved: boolean; // New field for marking risks as resolved
|
||||
riskOwner: string | null; // New field for setting a risk owner (nullable string)
|
||||
installationId: string,
|
||||
repositoryId: string,
|
||||
repositoryLink: string
|
||||
repositoryFullName: string
|
||||
status: string
|
||||
pusher: {
|
||||
name: string,
|
||||
email: string
|
||||
},
|
||||
organization: Schema.Types.ObjectId,
|
||||
}
|
||||
|
||||
const gitRisks = new Schema<GitRisks>({
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
startLine: {
|
||||
type: String,
|
||||
},
|
||||
endLine: {
|
||||
type: String,
|
||||
},
|
||||
startColumn: {
|
||||
type: String,
|
||||
},
|
||||
endColumn: {
|
||||
type: String,
|
||||
},
|
||||
file: {
|
||||
type: String,
|
||||
},
|
||||
symlinkFile: {
|
||||
type: String,
|
||||
},
|
||||
commit: {
|
||||
type: String,
|
||||
},
|
||||
entropy: {
|
||||
type: String,
|
||||
},
|
||||
author: {
|
||||
type: String,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
tags: {
|
||||
type: [String],
|
||||
},
|
||||
ruleID: {
|
||||
type: String,
|
||||
},
|
||||
fingerprint: {
|
||||
type: String,
|
||||
unique: true
|
||||
},
|
||||
isFalsePositive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isResolved: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
riskOwner: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
installationId: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
repositoryId: {
|
||||
type: String
|
||||
},
|
||||
repositoryLink: {
|
||||
type: String
|
||||
},
|
||||
repositoryFullName: {
|
||||
type: String
|
||||
},
|
||||
pusher: {
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
email: {
|
||||
type: String
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Organization",
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: [
|
||||
STATUS_RESOLVED_FALSE_POSITIVE,
|
||||
STATUS_RESOLVED_REVOKED,
|
||||
STATUS_RESOLVED_NOT_REVOKED,
|
||||
STATUS_UNRESOLVED
|
||||
],
|
||||
default: STATUS_UNRESOLVED
|
||||
}
|
||||
}, { timestamps: true });
|
||||
|
||||
const GitRisks = model<GitRisks>("GitRisks", gitRisks);
|
||||
|
||||
export default GitRisks;
|
||||
@@ -15,6 +15,7 @@ import password from "./password";
|
||||
import integration from "./integration";
|
||||
import integrationAuth from "./integrationAuth";
|
||||
import secretsFolder from "./secretsFolder";
|
||||
import secretScanning from "./secretScanning";
|
||||
|
||||
export {
|
||||
signup,
|
||||
@@ -34,4 +35,5 @@ export {
|
||||
integration,
|
||||
integrationAuth,
|
||||
secretsFolder,
|
||||
secretScanning
|
||||
};
|
||||
|
||||
80
backend/src/routes/v1/secretScanning.ts
Normal file
80
backend/src/routes/v1/secretScanning.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireOrganizationAuth,
|
||||
validateRequest,
|
||||
} from "../../middleware";
|
||||
import { body, param } from "express-validator";
|
||||
import { createInstallationSession, getCurrentOrganizationInstallationStatus, getRisksForOrganization, linkInstallationToOrganization, updateRisksStatus } from "../../controllers/v1/secretScanningController";
|
||||
import { ACCEPTED, ADMIN, MEMBER, OWNER } from "../../variables";
|
||||
|
||||
router.post(
|
||||
"/create-installation-session/organization/:organizationId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: ["jwt"],
|
||||
}),
|
||||
param("organizationId").exists().trim(),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
validateRequest,
|
||||
createInstallationSession
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/link-installation",
|
||||
requireAuth({
|
||||
acceptedAuthModes: ["jwt"],
|
||||
}),
|
||||
body("installationId").exists().trim(),
|
||||
body("sessionId").exists().trim(),
|
||||
validateRequest,
|
||||
linkInstallationToOrganization
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/installation-status/organization/:organizationId",
|
||||
requireAuth({
|
||||
acceptedAuthModes: ["jwt"],
|
||||
}),
|
||||
param("organizationId").exists().trim(),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
validateRequest,
|
||||
getCurrentOrganizationInstallationStatus
|
||||
);
|
||||
|
||||
router.get(
|
||||
"/organization/:organizationId/risks",
|
||||
requireAuth({
|
||||
acceptedAuthModes: ["jwt"],
|
||||
}),
|
||||
param("organizationId").exists().trim(),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
validateRequest,
|
||||
getRisksForOrganization
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/organization/:organizationId/risks/:riskId/status",
|
||||
requireAuth({
|
||||
acceptedAuthModes: ["jwt"],
|
||||
}),
|
||||
param("organizationId").exists().trim(),
|
||||
param("riskId").exists().trim(),
|
||||
requireOrganizationAuth({
|
||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||
acceptedStatuses: [ACCEPTED],
|
||||
}),
|
||||
validateRequest,
|
||||
updateRisksStatus
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -46,6 +46,15 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
|
||||
stack: error?.stack,
|
||||
});
|
||||
|
||||
export const ResourceNotFound = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.INFO,
|
||||
statusCode: error?.statusCode ?? 404,
|
||||
type: error?.type ?? "resource_not_found",
|
||||
message: error?.message ?? "The requested resource is not found",
|
||||
context: error?.context,
|
||||
stack: error?.stack,
|
||||
});
|
||||
|
||||
export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
|
||||
logLevel: error?.logLevel ?? LogLevel.ERROR,
|
||||
statusCode: error?.statusCode ?? 500,
|
||||
|
||||
@@ -58,6 +58,31 @@ services:
|
||||
networks:
|
||||
- infisical-dev
|
||||
|
||||
git-app:
|
||||
container_name: infisical-dev-git-app
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- mongo
|
||||
- smtp-server
|
||||
- backend
|
||||
- frontend
|
||||
volumes:
|
||||
- ./secret-engine/src:/app/src/ # mounted whole src to avoid missing reload on new files
|
||||
ports:
|
||||
- "3000:3001"
|
||||
build:
|
||||
context: ./secret-engine
|
||||
dockerfile: Dockerfile.dev
|
||||
command: npm run start
|
||||
env_file: ./secret-engine/.env
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
|
||||
networks:
|
||||
- infisical-dev
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
mongo:
|
||||
image: mongo
|
||||
container_name: infisical-dev-mongo
|
||||
|
||||
@@ -7,7 +7,8 @@ module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true
|
||||
es2021: true,
|
||||
"es6": true
|
||||
},
|
||||
extends: [
|
||||
"airbnb",
|
||||
@@ -70,6 +71,7 @@ module.exports = {
|
||||
],
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"simple-import-sort/exports": "warn",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"simple-import-sort/imports": [
|
||||
"warn",
|
||||
{
|
||||
|
||||
@@ -364,6 +364,14 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="/secret-scanning" passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === "/secret-scanning"}
|
||||
icon="system-outline-82-extension"
|
||||
>Secret scanning </MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/activity/${currentWorkspace?._id}`} passHref>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/activity/${currentWorkspace?._id}`}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const createNewIntegrationSession = (organizationId: string) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/create-installation-session/organization/${organizationId}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to create integration session");
|
||||
console.log("response", res)
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default createNewIntegrationSession;
|
||||
@@ -0,0 +1,22 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const getInstallationStatus = (organizationId: string) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/installation-status/organization/${organizationId}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).appInstallationComplete;
|
||||
}
|
||||
console.log("Failed to check installation status");
|
||||
console.log("response", res)
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default getInstallationStatus;
|
||||
@@ -0,0 +1,57 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
export type GitRisks = {
|
||||
id: string;
|
||||
description: string;
|
||||
startLine: string;
|
||||
endLine: string;
|
||||
startColumn: string;
|
||||
endColumn: string;
|
||||
match: string;
|
||||
secret: string;
|
||||
file: string;
|
||||
symlinkFile: string;
|
||||
commit: string;
|
||||
entropy: string;
|
||||
author: string;
|
||||
email: string;
|
||||
date: string;
|
||||
message: string;
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
|
||||
isFalsePositive: boolean; // New field for marking risks as false positives
|
||||
isResolved: boolean; // New field for marking risks as resolved
|
||||
riskOwner: string | null; // New field for setting a risk owner (nullable string)
|
||||
installationId: string,
|
||||
repositoryId: string,
|
||||
repositoryLink: string
|
||||
repositoryFullName: string
|
||||
pusher: {
|
||||
name: string,
|
||||
email: string
|
||||
},
|
||||
createdAt: string,
|
||||
organization: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const getRisksByOrganization = (oranizationId: string): Promise<GitRisks[]> =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${oranizationId}/risks`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return (await res.json()).risks;
|
||||
}
|
||||
console.log("Failed to fetch risks");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default getRisksByOrganization;
|
||||
@@ -0,0 +1,25 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const linkGitAppInstallationWithOrganization = (installationId: string, sessionId: string) =>
|
||||
SecurityClient.fetchCall("/api/v1/secret-scanning/link-installation", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
installationId,
|
||||
sessionId
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to link installation to organization");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default linkGitAppInstallationWithOrganization;
|
||||
33
frontend/src/pages/api/secret-scanning/updateRiskStatus.ts
Normal file
33
frontend/src/pages/api/secret-scanning/updateRiskStatus.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
|
||||
export enum RiskStatus {
|
||||
RESOLVED_FALSE_POSITIVE = "RESOLVED_FALSE_POSITIVE",
|
||||
RESOLVED_REVOKED = "RESOLVED_REVOKED",
|
||||
RESOLVED_NOT_REVOKED = "RESOLVED_NOT_REVOKED",
|
||||
UNRESOLVED = "UNRESOLVED",
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Will create a new integration session and return it for the given org
|
||||
* @returns
|
||||
*/
|
||||
const updateRiskStatus = (organizationId: string, riskId: string, status: RiskStatus) =>
|
||||
SecurityClient.fetchCall(`/api/v1/secret-scanning/organization/${organizationId}/risks/${riskId}/status`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status
|
||||
})
|
||||
}).then(async (res) => {
|
||||
if (res && res.status === 200) {
|
||||
return res.json();
|
||||
}
|
||||
console.log("Failed to link installation to organization");
|
||||
return undefined;
|
||||
});
|
||||
|
||||
export default updateRiskStatus;
|
||||
156
frontend/src/pages/secret-scanning/index.tsx
Normal file
156
frontend/src/pages/secret-scanning/index.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
import createNewIntegrationSession from "../api/secret-scanning/createSecretScanningSession";
|
||||
import getInstallationStatus from "../api/secret-scanning/getInstallationStatus";
|
||||
import getRisksByOrganization, { GitRisks } from "../api/secret-scanning/getRisksByOrganization";
|
||||
import linkGitAppInstallationWithOrganization from "../api/secret-scanning/linkGitAppInstallationWithOrganization";
|
||||
import { RiskStatus } from "../api/secret-scanning/updateRiskStatus";
|
||||
|
||||
export default function SecretScanning() {
|
||||
const router = useRouter()
|
||||
const { state, installationId} = router.query
|
||||
const [integrationEnabled, setIntegrationStatus] = useState(false)
|
||||
const [gitRisks, setGitRisks] = useState<GitRisks[]>([]);
|
||||
const [selectedRiskStatus, setSelectedRiskStatus] = useState("");
|
||||
|
||||
const handleSelectRiskStatusUpdate = (event: any) => {
|
||||
setSelectedRiskStatus(event.target.value);
|
||||
};
|
||||
|
||||
console.log("selectedRiskStatus===>", selectedRiskStatus)
|
||||
|
||||
useEffect(()=>{
|
||||
const fetchRisks = async () =>{
|
||||
const risks = await getRisksByOrganization(String(localStorage.getItem("orgData.id")))
|
||||
setGitRisks(risks)
|
||||
}
|
||||
|
||||
const linkInstallation = async () => {
|
||||
if (typeof state === "string" && typeof installationId === "string"){
|
||||
try {
|
||||
await linkGitAppInstallationWithOrganization(installationId as string, state as string)
|
||||
console.log("installation verification complete")
|
||||
}catch (e){
|
||||
console.log("app installation is stale, start new session", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fetchInstallationStatus = async () => {
|
||||
const status = await getInstallationStatus(String(localStorage.getItem("orgData.id")))
|
||||
setIntegrationStatus(status)
|
||||
}
|
||||
|
||||
fetchInstallationStatus()
|
||||
linkInstallation()
|
||||
fetchRisks()
|
||||
},[state, installationId])
|
||||
|
||||
const generateNewIntegrationSession = async () => {
|
||||
const session = await createNewIntegrationSession(String(localStorage.getItem("orgData.id")))
|
||||
router.push(`https://github.com/apps/infisical-radar/installations/new?state=${session.sessionId}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>Secret scanning</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
{/* <NavHeader pageName={"Secret scanning"} isProjectRelated={false} /> */}
|
||||
|
||||
<div className="text-left">
|
||||
{integrationEnabled ? (
|
||||
<b className="text-green-500">Git app is linked to this organization</b>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
onClick={generateNewIntegrationSession}
|
||||
>
|
||||
Integrate with GitHub
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Date
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Secret Type
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
View Risk
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Info
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="py-3 px-6 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{gitRisks.map((risk) => {
|
||||
return (
|
||||
<tr key={risk.ruleID}>
|
||||
<td className="py-4 px-6 whitespace-nowrap">{risk.createdAt}</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">{risk.ruleID}</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
<a
|
||||
href={`https://github.com/${risk.repositoryFullName}/blob/${risk.commit}/${risk.file}#L${risk.startLine}-L${risk.endLine}`}
|
||||
target="_blank"
|
||||
className="text-red-500" rel="noreferrer"
|
||||
>
|
||||
View Exposed Secret
|
||||
</a>
|
||||
</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
<div className="font-bold">
|
||||
<a href={`https://github.com/${risk.repositoryFullName}`}>
|
||||
{risk.repositoryFullName}
|
||||
</a>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span>{risk.file}</span><br/>
|
||||
<br/>
|
||||
<span className="font-bold">{risk.author}</span><br/>
|
||||
<span>{risk.email}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
{risk.isResolved ? "Resolved" : "Needs Attention"}
|
||||
</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
{risk.isResolved ? "Resolved" : "Needs Attention"}
|
||||
</td>
|
||||
<td className="py-4 px-6 whitespace-nowrap">
|
||||
<select
|
||||
value={selectedRiskStatus}
|
||||
onChange={handleSelectRiskStatusUpdate}
|
||||
className="block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option>Unresolved</option>
|
||||
<option value={RiskStatus.RESOLVED_FALSE_POSITIVE}>This is a false positive</option>
|
||||
<option value={RiskStatus.RESOLVED_REVOKED}>I have rotated the secret, resolve risk</option>
|
||||
<option value={RiskStatus.RESOLVED_NOT_REVOKED}>No rotate needed, resolve</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,6 +14,20 @@ server {
|
||||
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
||||
}
|
||||
|
||||
location /git-app-api {
|
||||
proxy_set_header X-Real-RIP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
|
||||
proxy_pass http://git-app:3000/;
|
||||
proxy_redirect off;
|
||||
# proxy_redirect http://localhost:8080/ http://frontend.example.com/;
|
||||
|
||||
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict";
|
||||
}
|
||||
|
||||
location / {
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
|
||||
3878
package-lock.json
generated
3878
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
secret-engine/.eslintrc
Normal file
14
secret-engine/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
// "extends": [
|
||||
// "eslint:recommended",
|
||||
// "plugin:@typescript-eslint/recommended"
|
||||
// ]
|
||||
}
|
||||
13
secret-engine/Dockerfile.dev
Normal file
13
secret-engine/Dockerfile.dev
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM node:18-slim
|
||||
WORKDIR app/
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
RUN npm cache clean --force
|
||||
COPY . .
|
||||
|
||||
RUN apt-get update && apt-get install -y bash curl && curl -1sLf \
|
||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.deb.sh' | bash \
|
||||
&& apt-get update && apt-get install -y infisical=0.8.1
|
||||
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
3011
secret-engine/package-lock.json
generated
3011
secret-engine/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,17 +13,24 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "npm run build && probot run ./lib/index.js",
|
||||
"dev": "npm run build && probot run ./lib/index.js",
|
||||
"start": "nodemon -e ts,yml --watch './**/*.ts' --exec npm run dev",
|
||||
"test": "jest"
|
||||
},
|
||||
"dependencies": {
|
||||
"probot": "^12.2.4"
|
||||
"mongoose": "^7.3.1",
|
||||
"probot": "^12.2.4",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.0",
|
||||
"@types/node": "^18.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||
"@typescript-eslint/parser": "^5.61.0",
|
||||
"eslint": "^8.44.0",
|
||||
"jest": "^29.0.0",
|
||||
"nock": "^13.0.5",
|
||||
"nodemon": "^2.0.22",
|
||||
"smee-client": "^1.2.2",
|
||||
"ts-jest": "^29.0.0",
|
||||
"typescript": "^4.1.3"
|
||||
|
||||
@@ -3,11 +3,9 @@ import { exec } from "child_process";
|
||||
import { writeFile, readFile, rm, mkdir, } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path"
|
||||
|
||||
// interface CommandResult {
|
||||
// stdout: string;
|
||||
// stderr: string;
|
||||
// }
|
||||
import mongoose from "mongoose";
|
||||
import GitRisks from "./models/gitRisks";
|
||||
import GitAppOrganizationInstallation from "./models/gitAppOrganizationInstallation";
|
||||
|
||||
type SecretMatch = {
|
||||
Description: string;
|
||||
@@ -31,17 +29,41 @@ type SecretMatch = {
|
||||
};
|
||||
|
||||
export = (app: Probot) => {
|
||||
app.on("pull_request", async (context) => {
|
||||
// create a check
|
||||
// connect to DB
|
||||
initDatabase()
|
||||
|
||||
app.on("installation.created", async (context) => {
|
||||
const { payload } = context;
|
||||
// console.log("payload==>", payload.installation.repository_selection)
|
||||
})
|
||||
|
||||
app.on("installation.deleted", async (context) => {
|
||||
const { payload } = context;
|
||||
const { installation, repositories } = payload;
|
||||
if (installation.repository_selection == "all") {
|
||||
await GitRisks.deleteMany({ installationId: installation.id })
|
||||
await GitAppOrganizationInstallation.deleteOne({ installationId: installation.id })
|
||||
} else {
|
||||
for (const repository of repositories) {
|
||||
await GitRisks.deleteMany({ repositoryId: repository.id })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
app.on("push", async (context) => {
|
||||
console.log("something was pushed")
|
||||
const { payload } = context;
|
||||
const { commits, repository } = payload;
|
||||
const { commits, repository, installation, } = payload;
|
||||
const [owner, repo] = repository.full_name.split('/');
|
||||
|
||||
const fingerPrints: any = []
|
||||
const installationLinkToOrgExists = await GitAppOrganizationInstallation.findOne({ installationId: installation.id }).lean()
|
||||
if (!installationLinkToOrgExists) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log("installation link does exist!")
|
||||
|
||||
const findingsByFingerprint: { [key: string]: SecretMatch; } = {}
|
||||
|
||||
for (const commit of commits) {
|
||||
for (const filepath of [...commit.added, ...commit.modified]) {
|
||||
try {
|
||||
@@ -57,7 +79,13 @@ export = (app: Probot) => {
|
||||
const findings = await scanContentAndGetFindings(`\n${fileContent}`) // to count lines correctly
|
||||
|
||||
for (const finding of findings) {
|
||||
fingerPrints.push(`${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`)
|
||||
const fingerPrint = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`
|
||||
finding.Fingerprint = fingerPrint
|
||||
finding.Commit = commit.id
|
||||
finding.File = filepath
|
||||
finding.Author = commit.author.name
|
||||
finding.Email = commit.author.email
|
||||
findingsByFingerprint[fingerPrint] = finding
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -66,7 +94,19 @@ export = (app: Probot) => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("fingerPrints==>", fingerPrints)
|
||||
// change to update
|
||||
for (const key in findingsByFingerprint) {
|
||||
await GitRisks.findOneAndUpdate({ fingerprint: findingsByFingerprint[key].Fingerprint },
|
||||
{
|
||||
...convertKeysToLowercase(findingsByFingerprint[key]),
|
||||
installationId: installation.id,
|
||||
organization: installationLinkToOrgExists.organizationId,
|
||||
repositoryFullName: repository.full_name,
|
||||
repositoryId: repository.id
|
||||
}, {
|
||||
upsert: true
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -149,3 +189,31 @@ function deleteTempFolder(folderPath: string): Promise<void> {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const initDatabase = async () => {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGO_URL);
|
||||
// allow empty strings to pass the required validator
|
||||
mongoose.Schema.Types.String.checkRequired(v => typeof v === "string");
|
||||
|
||||
console.log("Database connection established");
|
||||
|
||||
} catch (err) {
|
||||
console.log(`Unable to establish Database connection due to the error.\n${err}`);
|
||||
}
|
||||
|
||||
return mongoose.connection;
|
||||
}
|
||||
|
||||
function convertKeysToLowercase<T>(obj: T): T {
|
||||
const convertedObj = {} as T;
|
||||
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
const lowercaseKey = key.charAt(0).toLowerCase() + key.slice(1);
|
||||
convertedObj[lowercaseKey] = obj[key];
|
||||
}
|
||||
}
|
||||
|
||||
return convertedObj;
|
||||
}
|
||||
31
secret-engine/src/models/gitAppOrganizationInstallation.ts
Normal file
31
secret-engine/src/models/gitAppOrganizationInstallation.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
|
||||
type Installation = {
|
||||
installationId: string
|
||||
organizationId: string
|
||||
user: Schema.Types.ObjectId
|
||||
};
|
||||
|
||||
|
||||
const gitAppOrganizationInstallation = new Schema<Installation>({
|
||||
installationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
organizationId: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const GitAppOrganizationInstallation = model<Installation>("git_app_organization_installation", gitAppOrganizationInstallation);
|
||||
|
||||
export default GitAppOrganizationInstallation;
|
||||
147
secret-engine/src/models/gitRisks.ts
Normal file
147
secret-engine/src/models/gitRisks.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Schema, model } from "mongoose";
|
||||
export const STATUS_RESOLVED_FALSE_POSITIVE = 'RESOLVED_FALSE_POSITIVE';
|
||||
export const STATUS_RESOLVED_REVOKED = 'RESOLVED_REVOKED';
|
||||
export const STATUS_RESOLVED_NOT_REVOKED = 'RESOLVED_NOT_REVOKED';
|
||||
export const STATUS_UNRESOLVED = 'UNRESOLVED';
|
||||
|
||||
export type GitRisks = {
|
||||
id: string;
|
||||
description: string;
|
||||
startLine: string;
|
||||
endLine: string;
|
||||
startColumn: string;
|
||||
endColumn: string;
|
||||
match: string;
|
||||
secret: string;
|
||||
file: string;
|
||||
symlinkFile: string;
|
||||
commit: string;
|
||||
entropy: string;
|
||||
author: string;
|
||||
email: string;
|
||||
date: string;
|
||||
message: string;
|
||||
tags: string[];
|
||||
ruleID: string;
|
||||
fingerprint: string;
|
||||
|
||||
isFalsePositive: boolean; // New field for marking risks as false positives
|
||||
isResolved: boolean; // New field for marking risks as resolved
|
||||
riskOwner: string | null; // New field for setting a risk owner (nullable string)
|
||||
installationId: string,
|
||||
repositoryId: string,
|
||||
repositoryLink: string
|
||||
repositoryFullName: string
|
||||
status: string
|
||||
pusher: {
|
||||
name: string,
|
||||
email: string
|
||||
},
|
||||
organization: Schema.Types.ObjectId,
|
||||
}
|
||||
|
||||
const gitRisks = new Schema<GitRisks>({
|
||||
id: {
|
||||
type: String,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
},
|
||||
startLine: {
|
||||
type: String,
|
||||
},
|
||||
endLine: {
|
||||
type: String,
|
||||
},
|
||||
startColumn: {
|
||||
type: String,
|
||||
},
|
||||
endColumn: {
|
||||
type: String,
|
||||
},
|
||||
file: {
|
||||
type: String,
|
||||
},
|
||||
symlinkFile: {
|
||||
type: String,
|
||||
},
|
||||
commit: {
|
||||
type: String,
|
||||
},
|
||||
entropy: {
|
||||
type: String,
|
||||
},
|
||||
author: {
|
||||
type: String,
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
},
|
||||
date: {
|
||||
type: String,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
tags: {
|
||||
type: [String],
|
||||
},
|
||||
ruleID: {
|
||||
type: String,
|
||||
},
|
||||
fingerprint: {
|
||||
type: String,
|
||||
unique: true
|
||||
},
|
||||
isFalsePositive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isResolved: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
riskOwner: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
installationId: {
|
||||
type: String,
|
||||
require: true
|
||||
},
|
||||
repositoryId: {
|
||||
type: String
|
||||
},
|
||||
repositoryLink: {
|
||||
type: String
|
||||
},
|
||||
repositoryFullName: {
|
||||
type: String
|
||||
},
|
||||
pusher: {
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
email: {
|
||||
type: String
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Organization",
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: [
|
||||
STATUS_RESOLVED_FALSE_POSITIVE,
|
||||
STATUS_RESOLVED_REVOKED,
|
||||
STATUS_RESOLVED_NOT_REVOKED,
|
||||
STATUS_UNRESOLVED
|
||||
],
|
||||
default: STATUS_UNRESOLVED
|
||||
}
|
||||
}, { timestamps: true });
|
||||
|
||||
const GitRisks = model<GitRisks>("GitRisks", gitRisks);
|
||||
|
||||
export default GitRisks;
|
||||
@@ -25,7 +25,7 @@
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"strict": false /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
|
||||
Reference in New Issue
Block a user