@catladder/cli 1.135.0 → 1.136.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/catci +3 -0
- package/bin/catci-dev +3 -0
- package/dist/apps/catci/catci.d.ts +1 -0
- package/dist/apps/catci/catci.js +71 -0
- package/dist/apps/catci/catci.js.map +1 -0
- package/dist/apps/catci/commands/security/auditDocument.d.ts +19 -0
- package/dist/apps/catci/commands/security/auditDocument.js +90 -0
- package/dist/apps/catci/commands/security/auditDocument.js.map +1 -0
- package/dist/apps/catci/commands/security/commands.d.ts +2 -0
- package/dist/apps/catci/commands/security/commands.js +175 -0
- package/dist/apps/catci/commands/security/commands.js.map +1 -0
- package/dist/apps/catci/commands/security/createSecurityAuditMergeRequest.d.ts +9 -0
- package/dist/apps/catci/commands/security/createSecurityAuditMergeRequest.js +112 -0
- package/dist/apps/catci/commands/security/createSecurityAuditMergeRequest.js.map +1 -0
- package/dist/apps/catci/commands/security/evaluateSecurityAudit.d.ts +5 -0
- package/dist/apps/catci/commands/security/evaluateSecurityAudit.js +76 -0
- package/dist/apps/catci/commands/security/evaluateSecurityAudit.js.map +1 -0
- package/dist/apps/catci/commands/security/topics.json +112 -0
- package/dist/apps/cli/commands/project/commandSecurityEvaluate.d.ts +3 -0
- package/dist/apps/cli/commands/project/commandSecurityEvaluate.js +70 -0
- package/dist/apps/cli/commands/project/commandSecurityEvaluate.js.map +1 -0
- package/dist/apps/cli/commands/project/index.js +2 -0
- package/dist/apps/cli/commands/project/index.js.map +1 -1
- package/dist/bundles/catci/index.js +41 -0
- package/dist/bundles/catenv/index.js +1 -1
- package/dist/bundles/cli/index.js +3 -3
- package/dist/catci.d.ts +1 -0
- package/dist/catci.js +5 -0
- package/dist/catci.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/scripts/bundle +2 -0
- package/src/apps/catci/catci.ts +20 -0
- package/src/apps/catci/commands/security/auditDocument.ts +150 -0
- package/src/apps/catci/commands/security/commands.ts +146 -0
- package/src/apps/catci/commands/security/createSecurityAuditMergeRequest.ts +98 -0
- package/src/apps/catci/commands/security/evaluateSecurityAudit.ts +30 -0
- package/src/apps/catci/commands/security/topics.json +120 -0
- package/src/apps/cli/commands/project/commandSecurityEvaluate.ts +26 -0
- package/src/apps/cli/commands/project/index.ts +2 -0
- package/src/catci.ts +3 -0
package/package.json
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
}
|
|
53
53
|
],
|
|
54
54
|
"license": "MIT",
|
|
55
|
-
"version": "1.
|
|
55
|
+
"version": "1.136.0",
|
|
56
56
|
"scripts": {
|
|
57
57
|
"lint": "eslint \"src/**/*.ts\"",
|
|
58
58
|
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
@@ -63,17 +63,16 @@
|
|
|
63
63
|
"build:watch": "yarn tsc -w"
|
|
64
64
|
},
|
|
65
65
|
"bin": {
|
|
66
|
+
"catci": "./bin/catci",
|
|
66
67
|
"catenv": "./bin/catenv",
|
|
67
68
|
"catladder": "./bin/catladder"
|
|
68
69
|
},
|
|
69
|
-
"dependencies": {
|
|
70
|
-
"ts-node": "^10.9.1"
|
|
71
|
-
},
|
|
72
70
|
"engines": {
|
|
73
71
|
"node": ">=12.0.0"
|
|
74
72
|
},
|
|
75
73
|
"devDependencies": {
|
|
76
|
-
"@catladder/pipeline": "1.
|
|
74
|
+
"@catladder/pipeline": "1.136.0",
|
|
75
|
+
"@gitbeaker/rest": "^39.28.0",
|
|
77
76
|
"@kubernetes/client-node": "^0.16.2",
|
|
78
77
|
"@tsconfig/node14": "^1.0.1",
|
|
79
78
|
"@types/common-tags": "^1.8.0",
|
|
@@ -104,6 +103,8 @@
|
|
|
104
103
|
"open": "^8.4.0",
|
|
105
104
|
"prettier": "^2.5.1",
|
|
106
105
|
"tmp-promise": "^2.0.2",
|
|
106
|
+
"ts-node": "^10.9.1",
|
|
107
|
+
"ts-results-es": "^4.1.0-alpha.1",
|
|
107
108
|
"typescript": "^4.5.4",
|
|
108
109
|
"vorpal": "^1.12.0"
|
|
109
110
|
}
|
package/scripts/bundle
CHANGED
|
@@ -2,3 +2,5 @@
|
|
|
2
2
|
ncc build dist/catenv.js -o dist/bundles/catenv -e 'ts-node' $( ((SHOULD_MINIFY == 1)) && printf %s '-m')
|
|
3
3
|
|
|
4
4
|
ncc build dist/cli.js -o dist/bundles/cli -e 'ts-node' $( ((SHOULD_MINIFY == 1)) && printf %s '-m')
|
|
5
|
+
|
|
6
|
+
ncc build dist/catci.js -o dist/bundles/catci -e 'ts-node' $( ((SHOULD_MINIFY == 1)) && printf %s '-m')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Vorpal from "vorpal";
|
|
2
|
+
import packageInfo from "../../packageInfos";
|
|
3
|
+
import securityCommands from "./commands/security/commands";
|
|
4
|
+
|
|
5
|
+
export async function runCatCi() {
|
|
6
|
+
const vorpal = new Vorpal();
|
|
7
|
+
|
|
8
|
+
process.exitCode = 0;
|
|
9
|
+
vorpal.delimiter("catci $").history("catci").version(packageInfo.version);
|
|
10
|
+
|
|
11
|
+
securityCommands(vorpal);
|
|
12
|
+
|
|
13
|
+
const isInteractive = process.argv.length <= 2;
|
|
14
|
+
if (isInteractive) {
|
|
15
|
+
vorpal.log(`Catladder CI Tools 😻🔨 version ${packageInfo.version}`).show();
|
|
16
|
+
} else {
|
|
17
|
+
await vorpal.exec(process.argv.slice(2).join(" "));
|
|
18
|
+
process.exit();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import topicsJson from "./topics.json";
|
|
2
|
+
|
|
3
|
+
type Topic = {
|
|
4
|
+
description: string;
|
|
5
|
+
responsibles: number;
|
|
6
|
+
more: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const allTopics: Topic[] = topicsJson;
|
|
10
|
+
|
|
11
|
+
const checkYes = "✅";
|
|
12
|
+
const checkNo = "❌";
|
|
13
|
+
|
|
14
|
+
const checkPlaceholder = `${checkYes}/${checkNo}`;
|
|
15
|
+
const responsiblePlaceholder = "@...";
|
|
16
|
+
const rows = [
|
|
17
|
+
["Responsible", checkPlaceholder, "Description", "Note", "More Information"],
|
|
18
|
+
].concat(
|
|
19
|
+
allTopics.map((t) => [
|
|
20
|
+
Array(t.responsibles).fill(responsiblePlaceholder).join(", "),
|
|
21
|
+
checkPlaceholder,
|
|
22
|
+
t.description,
|
|
23
|
+
"",
|
|
24
|
+
t.more,
|
|
25
|
+
])
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function makeTable(rows: string[][]) {
|
|
29
|
+
const colWidths = calculateColumnWidths(rows);
|
|
30
|
+
|
|
31
|
+
return `
|
|
32
|
+
${makeRow(rows[0], colWidths, " ")}
|
|
33
|
+
${makeRow(
|
|
34
|
+
rows[0].map(() => ""),
|
|
35
|
+
colWidths,
|
|
36
|
+
"-"
|
|
37
|
+
)}
|
|
38
|
+
${rows
|
|
39
|
+
.slice(1)
|
|
40
|
+
.map((row) => makeRow(row, colWidths, " "))
|
|
41
|
+
.join("\n")}
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function calculateColumnWidths(rows: string[][]) {
|
|
46
|
+
const columnCount = rows[0].length;
|
|
47
|
+
return Array.from({ length: columnCount }, (_, i) => i).map((columnIndex) =>
|
|
48
|
+
Math.max(...rows.map((row) => row[columnIndex].length))
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeRow(row: string[], colWidths: number[], fillString: string) {
|
|
53
|
+
return `| ${row
|
|
54
|
+
.map((cell, i) => cell.padEnd(colWidths[i], fillString))
|
|
55
|
+
.join(" | ")} |`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function makeTemplate() {
|
|
59
|
+
return `
|
|
60
|
+
# Security Audit Report
|
|
61
|
+
|
|
62
|
+
A security audit report document is a comprehensive assessment of an application's security posture, containing security topics that auditors can mark to indicate the state of various security aspects.
|
|
63
|
+
|
|
64
|
+
It serves as a structured guide for security team to evaluate different security factors such as authentication, authorization, data encryption, input validation, and more.
|
|
65
|
+
|
|
66
|
+
## General Information
|
|
67
|
+
|
|
68
|
+
- Project Owner is @...
|
|
69
|
+
- Dev team:
|
|
70
|
+
- @...
|
|
71
|
+
- @...
|
|
72
|
+
- @...
|
|
73
|
+
|
|
74
|
+
## Project Security
|
|
75
|
+
|
|
76
|
+
${makeTable(rows)}
|
|
77
|
+
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type SecurityEvaluation = {
|
|
82
|
+
topics: {
|
|
83
|
+
description: string;
|
|
84
|
+
responsibles: string[];
|
|
85
|
+
note: string;
|
|
86
|
+
isUnknown: boolean;
|
|
87
|
+
isAnswered: boolean;
|
|
88
|
+
isSecured: boolean;
|
|
89
|
+
}[];
|
|
90
|
+
score: {
|
|
91
|
+
rating: number;
|
|
92
|
+
totalTopics: number;
|
|
93
|
+
answeredTopics: number;
|
|
94
|
+
securedTopics: number;
|
|
95
|
+
unknownTopics: number;
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export function evaluateDocument(document: string): SecurityEvaluation {
|
|
100
|
+
const rawRows =
|
|
101
|
+
document.match(/^\s*\|.*?\|\s*$/gm)?.map((row) => row.trim()) ?? [];
|
|
102
|
+
const matchedRows = rawRows
|
|
103
|
+
.map((row) => row.split("|").map((col) => col.trim()))
|
|
104
|
+
.slice(2);
|
|
105
|
+
const knownTopics = new Set(allTopics.map((t) => t.description));
|
|
106
|
+
|
|
107
|
+
const topics = matchedRows.map((col) => {
|
|
108
|
+
const responsibles = col[1].split(", ");
|
|
109
|
+
const answer = col[2];
|
|
110
|
+
const description = col[3];
|
|
111
|
+
const note = col[4];
|
|
112
|
+
|
|
113
|
+
const isUnknown = !knownTopics.has(description);
|
|
114
|
+
const isAnswered =
|
|
115
|
+
!isUnknown &&
|
|
116
|
+
!answer.includes(checkPlaceholder) &&
|
|
117
|
+
!responsibles.some((responsible) =>
|
|
118
|
+
responsible.includes(responsiblePlaceholder)
|
|
119
|
+
);
|
|
120
|
+
const isSecured = !isUnknown && isAnswered && answer.includes(checkYes);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
responsibles,
|
|
124
|
+
answer,
|
|
125
|
+
description,
|
|
126
|
+
note,
|
|
127
|
+
isUnknown,
|
|
128
|
+
isAnswered,
|
|
129
|
+
isSecured,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const totalTopics = allTopics.length;
|
|
134
|
+
const answeredTopics = topics.filter((t) => t.isAnswered).length;
|
|
135
|
+
const securedTopics = topics.filter((t) => t.isSecured).length;
|
|
136
|
+
const unknownTopics = topics.filter((t) => t.isUnknown).length;
|
|
137
|
+
|
|
138
|
+
const rating = Math.round((securedTopics / totalTopics) * 100);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
topics,
|
|
142
|
+
score: {
|
|
143
|
+
rating,
|
|
144
|
+
totalTopics,
|
|
145
|
+
answeredTopics,
|
|
146
|
+
securedTopics,
|
|
147
|
+
unknownTopics,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type Vorpal from "vorpal";
|
|
2
|
+
import {
|
|
3
|
+
evaluateSecurityAudit,
|
|
4
|
+
makeSecurityAuditOverview,
|
|
5
|
+
} from "./evaluateSecurityAudit";
|
|
6
|
+
import { Gitlab } from "@gitbeaker/rest";
|
|
7
|
+
import {
|
|
8
|
+
SECURITY_AUDIT_FILE_NAME,
|
|
9
|
+
createSecurityAuditMergeRequest,
|
|
10
|
+
} from "./createSecurityAuditMergeRequest";
|
|
11
|
+
|
|
12
|
+
const GITLAB_HOST = "https://git.panter.ch";
|
|
13
|
+
|
|
14
|
+
export default function (vorpal: Vorpal) {
|
|
15
|
+
commandCiJob(vorpal);
|
|
16
|
+
commandEvaluate(vorpal);
|
|
17
|
+
commandCreate(vorpal);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function commandCiJob(vorpal: Vorpal) {
|
|
21
|
+
vorpal
|
|
22
|
+
.command(
|
|
23
|
+
"security-audit-ci-job <path> <token> <mainBranch> <projectId> <userId>",
|
|
24
|
+
`Evaluates security audit document. If the document can't be evaluated or does not exist, creates a new MR with security audit document template.
|
|
25
|
+
|
|
26
|
+
<path> root path of a project with security audit document (${SECURITY_AUDIT_FILE_NAME})
|
|
27
|
+
<token> gitlab token with 'api' scopes and permissions to create a new branch
|
|
28
|
+
<main-branch> main branch name
|
|
29
|
+
<project-id> project id to create security audit for
|
|
30
|
+
<user-id> gitlab user id that will be assignee of the audit
|
|
31
|
+
`
|
|
32
|
+
)
|
|
33
|
+
.action(async (args) => {
|
|
34
|
+
const evaluation = await evaluateSecurityAudit({ path: args.path });
|
|
35
|
+
|
|
36
|
+
if (evaluation.isErr()) {
|
|
37
|
+
console.log("could not evaluate security audit document");
|
|
38
|
+
console.log(
|
|
39
|
+
"creating new merge request with security audit template..."
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const { token, mainBranch, projectId, userId } = args;
|
|
43
|
+
const api = new Gitlab({
|
|
44
|
+
host: GITLAB_HOST,
|
|
45
|
+
token,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const mr = await createSecurityAuditMergeRequest({
|
|
49
|
+
api,
|
|
50
|
+
mainBranch,
|
|
51
|
+
projectId,
|
|
52
|
+
userId: parseInt(userId),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (mr.isErr()) {
|
|
56
|
+
console.error(
|
|
57
|
+
`could not create merge request with security audit template: ${mr.error}`
|
|
58
|
+
);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log("security audit merge request created successfully");
|
|
64
|
+
console.log(
|
|
65
|
+
`please finish the MR by updating SECURITY.md document: ${mr.value.web_url}`
|
|
66
|
+
);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (evaluation.value.score.answeredTopics === 0) {
|
|
72
|
+
console.error("audit document has no answered topics");
|
|
73
|
+
console.error(
|
|
74
|
+
`please answer security topics in ${SECURITY_AUDIT_FILE_NAME} by adding responsible people and check/cross in the table`
|
|
75
|
+
);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
process.exitCode = 0;
|
|
81
|
+
console.log(makeSecurityAuditOverview(evaluation.value));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function commandEvaluate(vorpal: Vorpal) {
|
|
86
|
+
vorpal
|
|
87
|
+
.command(
|
|
88
|
+
"security-audit-evaluate <path>",
|
|
89
|
+
"Evaluates security audit document in given <path>"
|
|
90
|
+
)
|
|
91
|
+
.action(async (args) => {
|
|
92
|
+
console.log("evaluating security audit document...");
|
|
93
|
+
|
|
94
|
+
const result = await evaluateSecurityAudit({ path: args.path });
|
|
95
|
+
if (result.isErr()) {
|
|
96
|
+
console.error(result.error);
|
|
97
|
+
console.error(
|
|
98
|
+
`please make sure the security audit document ${SECURITY_AUDIT_FILE_NAME} is in the repository`
|
|
99
|
+
);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
} else {
|
|
102
|
+
console.log(makeSecurityAuditOverview(result.value));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function commandCreate(vorpal: Vorpal) {
|
|
108
|
+
vorpal
|
|
109
|
+
.command(
|
|
110
|
+
"security-audit-create <token> <mainBranch> <projectId> <userId>",
|
|
111
|
+
`Creates a MR in given project with the latest security audit template document
|
|
112
|
+
|
|
113
|
+
<token> gitlab token with 'api' scopes and permissions to create a new branch
|
|
114
|
+
<main-branch> main branch name
|
|
115
|
+
<project-id> project id to create security audit for
|
|
116
|
+
<user-id> gitlab user id that will be assignee of the audit
|
|
117
|
+
`
|
|
118
|
+
)
|
|
119
|
+
.action(async (args) => {
|
|
120
|
+
const { token, mainBranch, projectId, userId } = args;
|
|
121
|
+
|
|
122
|
+
const api = new Gitlab({
|
|
123
|
+
host: GITLAB_HOST,
|
|
124
|
+
token,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = await createSecurityAuditMergeRequest({
|
|
128
|
+
api,
|
|
129
|
+
mainBranch,
|
|
130
|
+
projectId,
|
|
131
|
+
userId: parseInt(userId),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (result.isErr()) {
|
|
135
|
+
console.error(
|
|
136
|
+
`could not create security audit merge request: ${result.error}`
|
|
137
|
+
);
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
} else {
|
|
140
|
+
console.log("security audit merge request created successfully");
|
|
141
|
+
console.log(
|
|
142
|
+
`please finish the MR by updating SECURITY.md document: ${result.value.web_url}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Gitlab } from "@gitbeaker/core";
|
|
2
|
+
import { Err, Result } from "ts-results-es";
|
|
3
|
+
import { makeTemplate } from "./auditDocument";
|
|
4
|
+
|
|
5
|
+
function makeDatedBranchName(branchName: string) {
|
|
6
|
+
const date = new Date().toISOString().slice(0, -5).replaceAll(/[:.T]/g, "-");
|
|
7
|
+
return `${branchName}-${date}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MR_TITLE = "Draft: chore(security): add security audit document";
|
|
11
|
+
export const SECURITY_AUDIT_FILE_NAME = "SECURITY.md" as const;
|
|
12
|
+
|
|
13
|
+
export async function createSecurityAuditMergeRequest({
|
|
14
|
+
projectId,
|
|
15
|
+
mainBranch,
|
|
16
|
+
userId,
|
|
17
|
+
api,
|
|
18
|
+
}: {
|
|
19
|
+
projectId: string;
|
|
20
|
+
mainBranch: string;
|
|
21
|
+
userId: number;
|
|
22
|
+
api: Gitlab;
|
|
23
|
+
}) {
|
|
24
|
+
const mrs = (
|
|
25
|
+
await Result.wrapAsync(() =>
|
|
26
|
+
api.MergeRequests.all({
|
|
27
|
+
state: "opened",
|
|
28
|
+
wip: "yes",
|
|
29
|
+
labels: "security-audit",
|
|
30
|
+
})
|
|
31
|
+
)
|
|
32
|
+
).mapErr(() => `could not search for existing merge requests` as const);
|
|
33
|
+
if (mrs.isErr()) return mrs;
|
|
34
|
+
|
|
35
|
+
const existingMr = mrs.value[0];
|
|
36
|
+
if (existingMr)
|
|
37
|
+
return Err(
|
|
38
|
+
`open merge request with security audit already exists: ${existingMr.web_url}`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const auditTemplate = Result.wrap(() => makeTemplate()).mapErr(
|
|
42
|
+
() => "could not make security audit template document" as const
|
|
43
|
+
);
|
|
44
|
+
if (auditTemplate.isErr()) return auditTemplate;
|
|
45
|
+
|
|
46
|
+
const branch = (
|
|
47
|
+
await Result.wrapAsync(() =>
|
|
48
|
+
api.Branches.create(
|
|
49
|
+
projectId,
|
|
50
|
+
makeDatedBranchName("chore/security-audit"),
|
|
51
|
+
mainBranch
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
).mapErr((e) => {
|
|
55
|
+
console.log(e);
|
|
56
|
+
return "could not create branch" as const;
|
|
57
|
+
});
|
|
58
|
+
if (branch.isErr()) return branch;
|
|
59
|
+
|
|
60
|
+
const commit = (
|
|
61
|
+
await Result.wrapAsync(() =>
|
|
62
|
+
api.Commits.create(
|
|
63
|
+
projectId,
|
|
64
|
+
branch.value.name,
|
|
65
|
+
"chore(security): add empty security audit document template",
|
|
66
|
+
[
|
|
67
|
+
{
|
|
68
|
+
action: "create",
|
|
69
|
+
filePath: SECURITY_AUDIT_FILE_NAME,
|
|
70
|
+
content: auditTemplate.value,
|
|
71
|
+
encoding: "text",
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
).mapErr(() => "could not create commit" as const);
|
|
77
|
+
if (commit.isErr()) return commit;
|
|
78
|
+
|
|
79
|
+
const mr = (
|
|
80
|
+
await Result.wrapAsync(() =>
|
|
81
|
+
api.MergeRequests.create(
|
|
82
|
+
projectId,
|
|
83
|
+
branch.value.name,
|
|
84
|
+
mainBranch,
|
|
85
|
+
MR_TITLE,
|
|
86
|
+
{
|
|
87
|
+
description: `Please follow and update security audit document in \`${SECURITY_AUDIT_FILE_NAME}\`.`,
|
|
88
|
+
assigneeId: userId,
|
|
89
|
+
squash: true,
|
|
90
|
+
labels: "security-audit",
|
|
91
|
+
removeSourceBranch: true,
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
).mapErr(() => "could not create merge request" as const);
|
|
96
|
+
|
|
97
|
+
return mr;
|
|
98
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Result } from "ts-results-es";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { SECURITY_AUDIT_FILE_NAME } from "./createSecurityAuditMergeRequest";
|
|
5
|
+
import type { SecurityEvaluation } from "./auditDocument";
|
|
6
|
+
import { evaluateDocument } from "./auditDocument";
|
|
7
|
+
|
|
8
|
+
export async function evaluateSecurityAudit({ path }: { path: string }) {
|
|
9
|
+
return (
|
|
10
|
+
await Result.wrapAsync(async () => {
|
|
11
|
+
const filePath = join(path, SECURITY_AUDIT_FILE_NAME);
|
|
12
|
+
const docData = await readFile(filePath);
|
|
13
|
+
const doc = docData.toString("utf-8");
|
|
14
|
+
return evaluateDocument(doc);
|
|
15
|
+
})
|
|
16
|
+
).mapErr((e) => `could not evaluate ${SECURITY_AUDIT_FILE_NAME}: ${e}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function makeSecurityAuditOverview(evaluation: SecurityEvaluation) {
|
|
20
|
+
const ratingToEmo = (r: number) => (r < 33 ? "🟥" : r < 66 ? "🟨" : "🟩");
|
|
21
|
+
|
|
22
|
+
return `Project security posture overview:
|
|
23
|
+
🧐 Total topics: ${evaluation.score.totalTopics}
|
|
24
|
+
🔒 Secured topics: ${evaluation.score.securedTopics}
|
|
25
|
+
📢 Answered topics: ${evaluation.score.answeredTopics}
|
|
26
|
+
❔ Unknown topics: ${evaluation.score.unknownTopics}
|
|
27
|
+
📊 Rating: ${ratingToEmo(evaluation.score.rating)} ${
|
|
28
|
+
evaluation.score.rating
|
|
29
|
+
}/100`;
|
|
30
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"description": "No API keys or secrets are stored in repository",
|
|
4
|
+
"responsibles": 1,
|
|
5
|
+
"more": ""
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"description": "The app does not provide password login",
|
|
9
|
+
"responsibles": 1,
|
|
10
|
+
"more": ""
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"description": "Passwords are not stored",
|
|
14
|
+
"responsibles": 1,
|
|
15
|
+
"more": ""
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"description":
|
|
19
|
+
"Passwords are stored hashed with salt and salt is not stored in the repository",
|
|
20
|
+
"responsibles": 1,
|
|
21
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/hash.md)"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"description": "Input that ends up in DOM is properly sanitized",
|
|
25
|
+
"responsibles": 1,
|
|
26
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/xss.md)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"description": "All user inputs have reasonable validations",
|
|
30
|
+
"responsibles": 1,
|
|
31
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/validation.md)"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"description": "The app is not using cookies",
|
|
35
|
+
"responsibles": 1,
|
|
36
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/cookies.md)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"description": "The app is using cookies and cookies are properly configured",
|
|
40
|
+
"responsibles": 1,
|
|
41
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/cookies.md)"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"description":
|
|
45
|
+
"The app uses JWT with a secret and the secret is not stored in the repository",
|
|
46
|
+
"responsibles": 1,
|
|
47
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/cookies.md)"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"description": "Authorization and user roles (RBAC) were reviewed thoroughly",
|
|
51
|
+
"responsibles": 2,
|
|
52
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/authorization.md)"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"description": "CORS headers do not use `*`",
|
|
56
|
+
"responsibles": 1,
|
|
57
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/cors.md)"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"description":
|
|
61
|
+
"CSP headers are properly configured (no `unsafe-inline` or `unsafe-eval`)",
|
|
62
|
+
"responsibles": 1,
|
|
63
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/csp.md)"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"description": "DoS defense mechanism is implemented",
|
|
67
|
+
"responsibles": 1,
|
|
68
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/dos.md)"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"description":
|
|
72
|
+
"YAML/XML parsing is not used or used YAML/XML parsers have disabled DTD",
|
|
73
|
+
"responsibles": 1,
|
|
74
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/dos.md)"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"description": "The app implements CSRF prevention",
|
|
78
|
+
"responsibles": 1,
|
|
79
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/csrf.md)"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"description": "The app has a rate limitter",
|
|
83
|
+
"responsibles": 1,
|
|
84
|
+
"more": ""
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"description":
|
|
88
|
+
"The app has disabled GraphQL introspection and schema registry",
|
|
89
|
+
"responsibles": 1,
|
|
90
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/graphql.md)"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"description": "The app has set GraphQL complexity query limits",
|
|
94
|
+
"responsibles": 1,
|
|
95
|
+
"more": "[guide](https://git.panter.ch/panter/security-guide/-/blob/main/docs/audit/graphql.md)"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"description": "`sitemap.xml` does not leak any routes with sensitive data",
|
|
99
|
+
"responsibles": 1,
|
|
100
|
+
"more": ""
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"description":
|
|
104
|
+
"Cloud storage is (private) configured to not leak any sensitive data publicly",
|
|
105
|
+
"responsibles": 1,
|
|
106
|
+
"more": ""
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"description":
|
|
110
|
+
"Security Dashboard checks weekly vulnerable dependencies https://dep.panter.swiss/",
|
|
111
|
+
"responsibles": 1,
|
|
112
|
+
"more": ""
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"description":
|
|
116
|
+
"The app has `.well-known/security.txt` https://securitytxt.org/",
|
|
117
|
+
"responsibles": 1,
|
|
118
|
+
"more": ""
|
|
119
|
+
}
|
|
120
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type Vorpal from "vorpal";
|
|
2
|
+
import {
|
|
3
|
+
evaluateSecurityAudit,
|
|
4
|
+
makeSecurityAuditOverview,
|
|
5
|
+
} from "../../../catci/commands/security/evaluateSecurityAudit";
|
|
6
|
+
import { getGitRoot } from "../../../../utils/projects";
|
|
7
|
+
|
|
8
|
+
export default async (vorpal: Vorpal) => {
|
|
9
|
+
vorpal
|
|
10
|
+
.command(
|
|
11
|
+
"project-security-evaluate",
|
|
12
|
+
"evaluate project's security audit document"
|
|
13
|
+
)
|
|
14
|
+
.action(async function () {
|
|
15
|
+
const gitRoot = await getGitRoot();
|
|
16
|
+
const result = await evaluateSecurityAudit({ path: gitRoot });
|
|
17
|
+
if (result.isErr()) {
|
|
18
|
+
console.error(
|
|
19
|
+
"Could not evaluate security audit document:",
|
|
20
|
+
result.error
|
|
21
|
+
);
|
|
22
|
+
} else {
|
|
23
|
+
console.log(makeSecurityAuditOverview(result.value));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
@@ -26,6 +26,7 @@ import commandTriggerCronjob from "./commandTriggerCronjob";
|
|
|
26
26
|
import commandOpenGrafanaPod from "./commandOpenGrafanaPod";
|
|
27
27
|
import commandSecretsClearBackups from "./commandSecretsClearBackups";
|
|
28
28
|
import commandProjectRestoreDb from "./cloudSql/commandProjectRestoreDb";
|
|
29
|
+
import commandSecurityEvaluate from "./commandSecurityEvaluate";
|
|
29
30
|
|
|
30
31
|
export default async (vorpal: Vorpal) => {
|
|
31
32
|
commandSetup(vorpal);
|
|
@@ -61,4 +62,5 @@ export default async (vorpal: Vorpal) => {
|
|
|
61
62
|
|
|
62
63
|
commandGetMyTotalWorktime(vorpal);
|
|
63
64
|
commandMigrateHelm3(vorpal);
|
|
65
|
+
commandSecurityEvaluate(vorpal);
|
|
64
66
|
};
|
package/src/catci.ts
ADDED