add v1 secret scanning

This commit is contained in:
Maidul Islam
2023-07-04 10:54:44 -04:00
parent 079d68c042
commit 9f6aa6b13e
29 changed files with 6019 additions and 1979 deletions
+23 -5
View File
@@ -1,6 +1,9 @@
{ {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "unused-imports"], "plugins": [
"@typescript-eslint",
"unused-imports"
],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/eslint-recommended",
@@ -8,14 +11,29 @@
], ],
"rules": { "rules": {
"no-console": 2, "no-console": 2,
"quotes": ["error", "double", { "avoidEscape": true }], "quotes": [
"comma-dangle": ["error", "only-multiline"], "error",
"double",
{
"avoidEscape": true
}
],
"comma-dangle": [
"error",
"only-multiline"
],
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error", "unused-imports/no-unused-imports": "error",
"@typescript-eslint/no-empty-function": "off",
"unused-imports/no-unused-vars": [ "unused-imports/no-unused-vars": [
"warn", "warn",
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" } {
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
], ],
"sort-imports": ["error", { "ignoreDeclarationSort": true }] "sort-imports": "off"
} }
} }
+2
View File
@@ -13,6 +13,7 @@ import * as signupController from "./signupController";
import * as userActionController from "./userActionController"; import * as userActionController from "./userActionController";
import * as userController from "./userController"; import * as userController from "./userController";
import * as workspaceController from "./workspaceController"; import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController";
export { export {
authController, authController,
@@ -30,4 +31,5 @@ export {
userActionController, userActionController,
userController, userController,
workspaceController, workspaceController,
secretScanningController
}; };
@@ -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)
}
+3 -1
View File
@@ -34,6 +34,7 @@ import {
organization as v1OrganizationRouter, organization as v1OrganizationRouter,
password as v1PasswordRouter, password as v1PasswordRouter,
secret as v1SecretRouter, secret as v1SecretRouter,
secretScanning as v1SecretScanningRouter,
secretsFolder as v1SecretsFolder, secretsFolder as v1SecretsFolder,
serviceToken as v1ServiceTokenRouter, serviceToken as v1ServiceTokenRouter,
signup as v1SignupRouter, signup as v1SignupRouter,
@@ -42,8 +43,8 @@ import {
workspace as v1WorkspaceRouter, workspace as v1WorkspaceRouter,
} from "./routes/v1"; } from "./routes/v1";
import { import {
signup as v2SignupRouter,
auth as v2AuthRouter, auth as v2AuthRouter,
signup as v2SignupRouter,
users as v2UsersRouter, users as v2UsersRouter,
organizations as v2OrganizationsRouter, organizations as v2OrganizationsRouter,
workspace as v2WorkspaceRouter, workspace as v2WorkspaceRouter,
@@ -125,6 +126,7 @@ const main = async () => {
app.use("/api/v1/integration", v1IntegrationRouter); app.use("/api/v1/integration", v1IntegrationRouter);
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter); app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
app.use("/api/v1/folders", v1SecretsFolder); app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
@@ -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;
@@ -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
View 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;
+2
View File
@@ -15,6 +15,7 @@ import password from "./password";
import integration from "./integration"; import integration from "./integration";
import integrationAuth from "./integrationAuth"; import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder"; import secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning";
export { export {
signup, signup,
@@ -34,4 +35,5 @@ export {
integration, integration,
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
secretScanning
}; };
+80
View 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;
+9
View File
@@ -46,6 +46,15 @@ export const BadRequestError = (error?: Partial<RequestErrorContext>) => new Req
stack: error?.stack, 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({ export const InternalServerError = (error?: Partial<RequestErrorContext>) => new RequestError({
logLevel: error?.logLevel ?? LogLevel.ERROR, logLevel: error?.logLevel ?? LogLevel.ERROR,
statusCode: error?.statusCode ?? 500, statusCode: error?.statusCode ?? 500,
+25
View File
@@ -58,6 +58,31 @@ services:
networks: networks:
- infisical-dev - 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: mongo:
image: mongo image: mongo
container_name: infisical-dev-mongo container_name: infisical-dev-mongo
+3 -1
View File
@@ -7,7 +7,8 @@ module.exports = {
root: true, root: true,
env: { env: {
browser: true, browser: true,
es2021: true es2021: true,
"es6": true
}, },
extends: [ extends: [
"airbnb", "airbnb",
@@ -70,6 +71,7 @@ module.exports = {
], ],
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"simple-import-sort/exports": "warn", "simple-import-sort/exports": "warn",
"@typescript-eslint/no-empty-function": "off",
"simple-import-sort/imports": [ "simple-import-sort/imports": [
"warn", "warn",
{ {
@@ -364,6 +364,14 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem> </MenuItem>
</a> </a>
</Link> </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> <Link href={`/activity/${currentWorkspace?._id}`} passHref>
<MenuItem <MenuItem
isSelected={router.asPath === `/activity/${currentWorkspace?._id}`} 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;
@@ -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;
@@ -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
View File
@@ -14,6 +14,20 @@ server {
proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; 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 / { location / {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
+1939 -1939
View File
File diff suppressed because it is too large Load Diff
+14
View 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
View 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" ]
+2996 -15
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -13,17 +13,24 @@
], ],
"scripts": { "scripts": {
"build": "tsc", "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" "test": "jest"
}, },
"dependencies": { "dependencies": {
"probot": "^12.2.4" "mongoose": "^7.3.1",
"probot": "^12.2.4",
"ts-node": "^10.9.1"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.0.0", "@types/jest": "^29.0.0",
"@types/node": "^18.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", "jest": "^29.0.0",
"nock": "^13.0.5", "nock": "^13.0.5",
"nodemon": "^2.0.22",
"smee-client": "^1.2.2", "smee-client": "^1.2.2",
"ts-jest": "^29.0.0", "ts-jest": "^29.0.0",
"typescript": "^4.1.3" "typescript": "^4.1.3"
+80 -12
View File
@@ -3,11 +3,9 @@ import { exec } from "child_process";
import { writeFile, readFile, rm, mkdir, } from "fs"; import { writeFile, readFile, rm, mkdir, } from "fs";
import { tmpdir } from "os"; import { tmpdir } from "os";
import { join } from "path" import { join } from "path"
import mongoose from "mongoose";
// interface CommandResult { import GitRisks from "./models/gitRisks";
// stdout: string; import GitAppOrganizationInstallation from "./models/gitAppOrganizationInstallation";
// stderr: string;
// }
type SecretMatch = { type SecretMatch = {
Description: string; Description: string;
@@ -31,17 +29,41 @@ type SecretMatch = {
}; };
export = (app: Probot) => { export = (app: Probot) => {
app.on("pull_request", async (context) => { // connect to DB
// create a check 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) => { app.on("push", async (context) => {
console.log("something was pushed")
const { payload } = context; const { payload } = context;
const { commits, repository } = payload; const { commits, repository, installation, } = payload;
const [owner, repo] = repository.full_name.split('/'); 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 commit of commits) {
for (const filepath of [...commit.added, ...commit.modified]) { for (const filepath of [...commit.added, ...commit.modified]) {
try { try {
@@ -57,7 +79,13 @@ export = (app: Probot) => {
const findings = await scanContentAndGetFindings(`\n${fileContent}`) // to count lines correctly const findings = await scanContentAndGetFindings(`\n${fileContent}`) // to count lines correctly
for (const finding of findings) { 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) { } 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;
}
@@ -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
View 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;
+1 -1
View File
@@ -25,7 +25,7 @@
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "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'). */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */ /* 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. */ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */ // "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */