@sectorzero/cli 0.0.0-stage → 1.0.4
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/dist/index.js +225 -0
- package/package.json +31 -6
- package/src/index.ts +154 -0
- package/src/sarif.ts +145 -0
- package/tsconfig.json +16 -0
- package/tsup.config.ts +10 -0
- package/README.md +0 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env Node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
"use strict";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
19
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
20
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
21
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
22
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
23
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
24
|
+
mod
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
// src/index.ts
|
|
28
|
+
var import_commander = require("commander");
|
|
29
|
+
var import_picocolors = __toESM(require("picocolors"));
|
|
30
|
+
var import_ora = __toESM(require("ora"));
|
|
31
|
+
var path = __toESM(require("path"));
|
|
32
|
+
var fs = __toESM(require("fs"));
|
|
33
|
+
var import_engine_scanner = require("@sectorzero/engine-scanner");
|
|
34
|
+
var import_engine_analyzer = require("@sectorzero/engine-analyzer");
|
|
35
|
+
var import_engine_fixer = require("@sectorzero/engine-fixer");
|
|
36
|
+
|
|
37
|
+
// src/sarif.ts
|
|
38
|
+
function generateSarifReport(findings, rootDir) {
|
|
39
|
+
const ruleMap = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const finding of findings) {
|
|
41
|
+
if (!ruleMap.has(finding.ruleId)) {
|
|
42
|
+
ruleMap.set(finding.ruleId, finding);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const sarifRules = Array.from(ruleMap.values()).map((finding) => {
|
|
46
|
+
let level = "warning";
|
|
47
|
+
if (finding.severity === "CRITICAL" || finding.severity === "HIGH") {
|
|
48
|
+
level = "error";
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
id: finding.ruleId,
|
|
52
|
+
name: finding.ruleId,
|
|
53
|
+
shortDescription: {
|
|
54
|
+
text: finding.detailedExplanation?.summary || `Vulnerability detected: ${finding.ruleId}`
|
|
55
|
+
},
|
|
56
|
+
fullDescription: {
|
|
57
|
+
text: finding.detailedExplanation?.impact || `Reachable issue found for ${finding.ruleId}.`
|
|
58
|
+
},
|
|
59
|
+
defaultConfiguration: { level },
|
|
60
|
+
help: {
|
|
61
|
+
text: finding.detailedExplanation?.remediationGuide || "Remediate this issue using warden fix.",
|
|
62
|
+
markdown: `**Impact:** ${finding.detailedExplanation?.impact || "High"}
|
|
63
|
+
|
|
64
|
+
**Remediation:** ${finding.detailedExplanation?.remediationGuide || "Move secrets to environment variables or parameterize queries."}`
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
const sarifResults = findings.map((finding) => {
|
|
69
|
+
let level = "warning";
|
|
70
|
+
if (finding.severity === "CRITICAL" || finding.severity === "HIGH") {
|
|
71
|
+
level = "error";
|
|
72
|
+
}
|
|
73
|
+
let relativeUri = finding.location.file;
|
|
74
|
+
if (relativeUri.startsWith(rootDir)) {
|
|
75
|
+
relativeUri = relativeUri.replace(rootDir, "").replace(/^[/\\]/, "");
|
|
76
|
+
}
|
|
77
|
+
relativeUri = relativeUri.replace(/\\/g, "/");
|
|
78
|
+
return {
|
|
79
|
+
ruleId: finding.ruleId,
|
|
80
|
+
level,
|
|
81
|
+
message: {
|
|
82
|
+
text: finding.detailedExplanation?.summary || `Reachable ${finding.ruleId} vulnerability.`
|
|
83
|
+
},
|
|
84
|
+
locations: [
|
|
85
|
+
{
|
|
86
|
+
physicalLocation: {
|
|
87
|
+
artifactLocation: { uri: relativeUri },
|
|
88
|
+
region: {
|
|
89
|
+
startLine: finding.location.line || 1,
|
|
90
|
+
startColumn: finding.location.column || 1
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
99
|
+
version: "2.1.0",
|
|
100
|
+
runs: [
|
|
101
|
+
{
|
|
102
|
+
tool: {
|
|
103
|
+
driver: {
|
|
104
|
+
name: "Warden AST Engine",
|
|
105
|
+
version: "1.0.0",
|
|
106
|
+
informationUri: "https://github.com/sectorzero-hq/warden",
|
|
107
|
+
rules: sarifRules
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
results: sarifResults
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/index.ts
|
|
117
|
+
var program = new import_commander.Command();
|
|
118
|
+
function getWidth() {
|
|
119
|
+
return Math.min(process.stdout.columns || 80, 100);
|
|
120
|
+
}
|
|
121
|
+
function centerText(text) {
|
|
122
|
+
const width = getWidth();
|
|
123
|
+
const padding = Math.max(0, Math.floor((width - text.length) / 2));
|
|
124
|
+
return " ".repeat(padding) + text;
|
|
125
|
+
}
|
|
126
|
+
function printHeader(targetPath) {
|
|
127
|
+
const width = getWidth();
|
|
128
|
+
const heavyDivider = import_picocolors.default.cyan("\u2550".repeat(width));
|
|
129
|
+
console.log(`
|
|
130
|
+
${heavyDivider}`);
|
|
131
|
+
console.log(centerText(import_picocolors.default.bold(import_picocolors.default.cyan("SECTOR ZERO WARDEN CLI"))));
|
|
132
|
+
console.log(centerText(import_picocolors.default.gray(`Target Directory: ${targetPath}`)));
|
|
133
|
+
console.log(`${heavyDivider}
|
|
134
|
+
`);
|
|
135
|
+
}
|
|
136
|
+
program.name("warden").description("Sector Zero Warden - AST Security Scanner & Automated Remediation Pipeline").version("1.0.0");
|
|
137
|
+
program.command("scan").description("Run reachability analysis and AST scan on target directory").argument("[directory]", "Path to source code directory", ".").option("--fix", "Automatically apply AST codemod fixes in-place").option("--json", "Output results in raw JSON format").option("--sarif [outputPath]", "Export findings in SARIF v2.1.0 format (default: warden-results.sarif)").action((directory, options) => {
|
|
138
|
+
const targetDir = path.resolve(process.cwd(), directory);
|
|
139
|
+
if (!fs.existsSync(targetDir)) {
|
|
140
|
+
console.error(import_picocolors.default.red(`Directory not found: ${targetDir}`));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const isSilentMode = Boolean(options.json || options.sarif);
|
|
144
|
+
const spinner = (0, import_ora.default)({ text: "Scanning source files and building AST graph...", color: "cyan" });
|
|
145
|
+
if (!isSilentMode) {
|
|
146
|
+
printHeader(targetDir);
|
|
147
|
+
spinner.start();
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const scanner = new import_engine_scanner.ASTScanner();
|
|
151
|
+
const analyzer = new import_engine_analyzer.ReachabilityAnalyzer();
|
|
152
|
+
const fixer = new import_engine_fixer.RemediationEngine();
|
|
153
|
+
const rawFindings = scanner.scanDirectory(targetDir);
|
|
154
|
+
const validatedFindings = analyzer.analyzeRepository(rawFindings);
|
|
155
|
+
const reachableFindings = validatedFindings.filter((f) => f.isReachable);
|
|
156
|
+
if (options.json) {
|
|
157
|
+
console.log(JSON.stringify(validatedFindings, null, 2));
|
|
158
|
+
process.exit(reachableFindings.length > 0 ? 1 : 0);
|
|
159
|
+
}
|
|
160
|
+
if (options.sarif) {
|
|
161
|
+
const sarifLog = generateSarifReport(reachableFindings, targetDir);
|
|
162
|
+
const fileName = typeof options.sarif === "string" ? options.sarif : "warden-results.sarif";
|
|
163
|
+
const outputPath = path.resolve(process.cwd(), fileName);
|
|
164
|
+
fs.writeFileSync(outputPath, JSON.stringify(sarifLog, null, 2), "utf-8");
|
|
165
|
+
console.log(import_picocolors.default.green(`\u2714 Exported SARIF 2.1.0 report to: ${outputPath}`));
|
|
166
|
+
process.exit(reachableFindings.length > 0 ? 1 : 0);
|
|
167
|
+
}
|
|
168
|
+
spinner.succeed(import_picocolors.default.green("Analysis Complete!"));
|
|
169
|
+
console.log(`
|
|
170
|
+
${import_picocolors.default.bold("Scan Summary:")}`);
|
|
171
|
+
console.log(` \u2022 Candidate Findings Found : ${import_picocolors.default.yellow(rawFindings.length.toString())}`);
|
|
172
|
+
console.log(
|
|
173
|
+
` \u2022 Reachable Vulnerabilities: ${reachableFindings.length > 0 ? import_picocolors.default.red(reachableFindings.length.toString()) : import_picocolors.default.green("0")}
|
|
174
|
+
`
|
|
175
|
+
);
|
|
176
|
+
let successCount = 0;
|
|
177
|
+
let failCount = 0;
|
|
178
|
+
if (reachableFindings.length > 0) {
|
|
179
|
+
console.log(import_picocolors.default.bold(options.fix ? import_picocolors.default.yellow("Remediating Vulnerabilities:") : import_picocolors.default.red("Reachable Vulnerabilities Detected:")));
|
|
180
|
+
for (const finding of reachableFindings) {
|
|
181
|
+
if (options.fix) {
|
|
182
|
+
const fixResult = fixer.applyFixToFile(finding.location.file, finding);
|
|
183
|
+
const statusTag = fixResult.success ? import_picocolors.default.bgGreen(import_picocolors.default.black(import_picocolors.default.bold(" FIXED "))) : import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" FAILED ")));
|
|
184
|
+
if (fixResult.success) successCount++;
|
|
185
|
+
else failCount++;
|
|
186
|
+
console.log(`
|
|
187
|
+
${statusTag} ${import_picocolors.default.bold(finding.ruleId)}`);
|
|
188
|
+
console.log(` \u2022 File: ${import_picocolors.default.cyan(`${finding.location.file}:${finding.location.line}:${finding.location.column}`)}`);
|
|
189
|
+
console.log(` \u2022 Explanation: ${fixResult.explanation}`);
|
|
190
|
+
} else {
|
|
191
|
+
const statusTag = import_picocolors.default.bgRed(import_picocolors.default.white(import_picocolors.default.bold(" DETECTED ")));
|
|
192
|
+
console.log(`
|
|
193
|
+
${statusTag} ${import_picocolors.default.bold(finding.ruleId)}`);
|
|
194
|
+
console.log(` \u2022 File: ${import_picocolors.default.cyan(`${finding.location.file}:${finding.location.line}:${finding.location.column}`)}`);
|
|
195
|
+
console.log(` \u2022 Category: ${finding.category} | Severity: ${import_picocolors.default.red(finding.severity)}`);
|
|
196
|
+
console.log(` \u2022 Summary: ${finding.detailedExplanation?.summary}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
console.log("");
|
|
200
|
+
}
|
|
201
|
+
const heavyDivider = import_picocolors.default.cyan("\u2550".repeat(getWidth()));
|
|
202
|
+
console.log(centerText(import_picocolors.default.bold(import_picocolors.default.magenta("\u2500\u2500\u2500 EXECUTION COMPLETE \u2500\u2500\u2500"))));
|
|
203
|
+
if (options.fix) {
|
|
204
|
+
console.log(
|
|
205
|
+
centerText(
|
|
206
|
+
`Patched: ${import_picocolors.default.bold(import_picocolors.default.green(successCount.toString()))} \u2502 Failed: ${import_picocolors.default.bold(failCount > 0 ? import_picocolors.default.red(failCount.toString()) : import_picocolors.default.gray(failCount.toString()))}`
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
console.log(
|
|
211
|
+
centerText(
|
|
212
|
+
`Total Vulnerabilities: ${import_picocolors.default.bold(import_picocolors.default.red(reachableFindings.length.toString()))} (Run with --fix to remediate)`
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
console.log(`${heavyDivider}
|
|
217
|
+
`);
|
|
218
|
+
process.exit(reachableFindings.length > 0 && !options.fix ? 1 : 0);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (spinner.isSpinning) spinner.fail(import_picocolors.default.red(`Scan failed: ${err.message}`));
|
|
221
|
+
else console.error(import_picocolors.default.red(`Scan failed: ${err.message}`));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@sectorzero/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@sectorzero/cli",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"private": false,
|
|
5
|
+
"bin": {
|
|
6
|
+
"warden": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup src/index.ts --format cjs --clean",
|
|
11
|
+
"dev": "tsup src/index.ts --format cjs --watch"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"git-checks": false
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@sectorzero/engine-analyzer": "workspace:*",
|
|
19
|
+
"@sectorzero/engine-fixer": "workspace:*",
|
|
20
|
+
"@sectorzero/engine-scanner": "workspace:*",
|
|
21
|
+
"@sectorzero/shared-types": "workspace:*",
|
|
22
|
+
"commander": "^15.0.0",
|
|
23
|
+
"ora": "^9.4.1",
|
|
24
|
+
"picocolors": "^1.1.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^26.6.5",
|
|
28
|
+
"tsup": "^9.0.0",
|
|
29
|
+
"typescript": "^5.9.3"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env Node
|
|
2
|
+
// packages/cli/src/index.ts
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import { ASTScanner } from '@sectorzero/engine-scanner';
|
|
9
|
+
import { ReachabilityAnalyzer } from '@sectorzero/engine-analyzer';
|
|
10
|
+
import { RemediationEngine } from '@sectorzero/engine-fixer';
|
|
11
|
+
import { generateSarifReport } from './sarif';
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
function getWidth(): number {
|
|
16
|
+
return Math.min(process.stdout.columns || 80, 100);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function centerText(text: string): string {
|
|
20
|
+
const width = getWidth();
|
|
21
|
+
const padding = Math.max(0, Math.floor((width - text.length) / 2));
|
|
22
|
+
return ' '.repeat(padding) + text;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function printHeader(targetPath: string): void {
|
|
26
|
+
const width = getWidth();
|
|
27
|
+
const heavyDivider = pc.cyan('═'.repeat(width));
|
|
28
|
+
console.log(`\n${heavyDivider}`);
|
|
29
|
+
console.log(centerText(pc.bold(pc.cyan('SECTOR ZERO WARDEN CLI'))));
|
|
30
|
+
console.log(centerText(pc.gray(`Target Directory: ${targetPath}`)));
|
|
31
|
+
console.log(`${heavyDivider}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.name('warden')
|
|
36
|
+
.description('Sector Zero Warden - AST Security Scanner & Automated Remediation Pipeline')
|
|
37
|
+
.version('1.0.0');
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command('scan')
|
|
41
|
+
.description('Run reachability analysis and AST scan on target directory')
|
|
42
|
+
.argument('[directory]', 'Path to source code directory', '.')
|
|
43
|
+
.option('--fix', 'Automatically apply AST codemod fixes in-place')
|
|
44
|
+
.option('--json', 'Output results in raw JSON format')
|
|
45
|
+
.option('--sarif [outputPath]', 'Export findings in SARIF v2.1.0 format (default: warden-results.sarif)')
|
|
46
|
+
.action((directory: string, options: { fix?: boolean; json?: boolean; sarif?: boolean | string }) => {
|
|
47
|
+
const targetDir = path.resolve(process.cwd(), directory);
|
|
48
|
+
|
|
49
|
+
if (!fs.existsSync(targetDir)) {
|
|
50
|
+
console.error(pc.red(`Directory not found: ${targetDir}`));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isSilentMode = Boolean(options.json || options.sarif);
|
|
55
|
+
const spinner = ora({ text: 'Scanning source files and building AST graph...', color: 'cyan' });
|
|
56
|
+
|
|
57
|
+
if (!isSilentMode) {
|
|
58
|
+
printHeader(targetDir);
|
|
59
|
+
spinner.start();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const scanner = new ASTScanner();
|
|
64
|
+
const analyzer = new ReachabilityAnalyzer();
|
|
65
|
+
const fixer = new RemediationEngine();
|
|
66
|
+
|
|
67
|
+
const rawFindings = scanner.scanDirectory(targetDir);
|
|
68
|
+
const validatedFindings = analyzer.analyzeRepository(rawFindings);
|
|
69
|
+
const reachableFindings = validatedFindings.filter((f) => f.isReachable);
|
|
70
|
+
|
|
71
|
+
// JSON Output Export
|
|
72
|
+
if (options.json) {
|
|
73
|
+
console.log(JSON.stringify(validatedFindings, null, 2));
|
|
74
|
+
process.exit(reachableFindings.length > 0 ? 1 : 0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// SARIF 2.1.0 Export
|
|
78
|
+
if (options.sarif) {
|
|
79
|
+
const sarifLog = generateSarifReport(reachableFindings, targetDir);
|
|
80
|
+
const fileName = typeof options.sarif === 'string' ? options.sarif : 'warden-results.sarif';
|
|
81
|
+
const outputPath = path.resolve(process.cwd(), fileName);
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(outputPath, JSON.stringify(sarifLog, null, 2), 'utf-8');
|
|
84
|
+
console.log(pc.green(`✔ Exported SARIF 2.1.0 report to: ${outputPath}`));
|
|
85
|
+
process.exit(reachableFindings.length > 0 ? 1 : 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
spinner.succeed(pc.green('Analysis Complete!'));
|
|
89
|
+
|
|
90
|
+
console.log(`\n${pc.bold('Scan Summary:')}`);
|
|
91
|
+
console.log(` • Candidate Findings Found : ${pc.yellow(rawFindings.length.toString())}`);
|
|
92
|
+
console.log(
|
|
93
|
+
` • Reachable Vulnerabilities: ${
|
|
94
|
+
reachableFindings.length > 0 ? pc.red(reachableFindings.length.toString()) : pc.green('0')
|
|
95
|
+
}\n`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
let successCount = 0;
|
|
99
|
+
let failCount = 0;
|
|
100
|
+
|
|
101
|
+
if (reachableFindings.length > 0) {
|
|
102
|
+
console.log(pc.bold(options.fix ? pc.yellow('Remediating Vulnerabilities:') : pc.red('Reachable Vulnerabilities Detected:')));
|
|
103
|
+
|
|
104
|
+
for (const finding of reachableFindings) {
|
|
105
|
+
if (options.fix) {
|
|
106
|
+
const fixResult = fixer.applyFixToFile(finding.location.file, finding);
|
|
107
|
+
|
|
108
|
+
const statusTag = fixResult.success
|
|
109
|
+
? pc.bgGreen(pc.black(pc.bold(' FIXED ')))
|
|
110
|
+
: pc.bgRed(pc.white(pc.bold(' FAILED ')));
|
|
111
|
+
|
|
112
|
+
if (fixResult.success) successCount++;
|
|
113
|
+
else failCount++;
|
|
114
|
+
|
|
115
|
+
console.log(`\n ${statusTag} ${pc.bold(finding.ruleId)}`);
|
|
116
|
+
console.log(` • File: ${pc.cyan(`${finding.location.file}:${finding.location.line}:${finding.location.column}`)}`);
|
|
117
|
+
console.log(` • Explanation: ${fixResult.explanation}`);
|
|
118
|
+
} else {
|
|
119
|
+
const statusTag = pc.bgRed(pc.white(pc.bold(' DETECTED ')));
|
|
120
|
+
console.log(`\n ${statusTag} ${pc.bold(finding.ruleId)}`);
|
|
121
|
+
console.log(` • File: ${pc.cyan(`${finding.location.file}:${finding.location.line}:${finding.location.column}`)}`);
|
|
122
|
+
console.log(` • Category: ${finding.category} | Severity: ${pc.red(finding.severity)}`);
|
|
123
|
+
console.log(` • Summary: ${finding.detailedExplanation?.summary}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
console.log('');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const heavyDivider = pc.cyan('═'.repeat(getWidth()));
|
|
130
|
+
console.log(centerText(pc.bold(pc.magenta('─── EXECUTION COMPLETE ───'))));
|
|
131
|
+
if (options.fix) {
|
|
132
|
+
console.log(
|
|
133
|
+
centerText(
|
|
134
|
+
`Patched: ${pc.bold(pc.green(successCount.toString()))} │ Failed: ${pc.bold(failCount > 0 ? pc.red(failCount.toString()) : pc.gray(failCount.toString()))}`
|
|
135
|
+
)
|
|
136
|
+
);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(
|
|
139
|
+
centerText(
|
|
140
|
+
`Total Vulnerabilities: ${pc.bold(pc.red(reachableFindings.length.toString()))} (Run with --fix to remediate)`
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
console.log(`${heavyDivider}\n`);
|
|
145
|
+
|
|
146
|
+
process.exit(reachableFindings.length > 0 && !options.fix ? 1 : 0);
|
|
147
|
+
} catch (err: any) {
|
|
148
|
+
if (spinner.isSpinning) spinner.fail(pc.red(`Scan failed: ${err.message}`));
|
|
149
|
+
else console.error(pc.red(`Scan failed: ${err.message}`));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
program.parse(process.argv);
|
package/src/sarif.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ValidatedFinding } from '@sectorzero/engine-analyzer';
|
|
2
|
+
|
|
3
|
+
export interface SarifLog {
|
|
4
|
+
$schema: string;
|
|
5
|
+
version: string;
|
|
6
|
+
runs: Array<{
|
|
7
|
+
tool: {
|
|
8
|
+
driver: {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
informationUri: string;
|
|
12
|
+
rules: Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
shortDescription: {
|
|
16
|
+
text: string;
|
|
17
|
+
};
|
|
18
|
+
fullDescription: {
|
|
19
|
+
text: string;
|
|
20
|
+
};
|
|
21
|
+
defaultConfiguration: {
|
|
22
|
+
level: 'error' | 'warning' | 'note';
|
|
23
|
+
};
|
|
24
|
+
help: {
|
|
25
|
+
text: string;
|
|
26
|
+
markdown: string;
|
|
27
|
+
};
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
results: Array<{
|
|
32
|
+
ruleId: string;
|
|
33
|
+
level: 'error' | 'warning' | 'note';
|
|
34
|
+
message: {
|
|
35
|
+
text: string;
|
|
36
|
+
};
|
|
37
|
+
locations: Array<{
|
|
38
|
+
physicalLocation: {
|
|
39
|
+
artifactLocation: {
|
|
40
|
+
uri: string;
|
|
41
|
+
};
|
|
42
|
+
region: {
|
|
43
|
+
startLine: number;
|
|
44
|
+
startColumn: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
}>;
|
|
48
|
+
}>;
|
|
49
|
+
}>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function generateSarifReport(
|
|
53
|
+
findings: ValidatedFinding[],
|
|
54
|
+
rootDir: string
|
|
55
|
+
): SarifLog {
|
|
56
|
+
const ruleMap = new Map<string, ValidatedFinding>();
|
|
57
|
+
|
|
58
|
+
for (const finding of findings) {
|
|
59
|
+
if (!ruleMap.has(finding.ruleId)) {
|
|
60
|
+
ruleMap.set(finding.ruleId, finding);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const sarifRules = Array.from(ruleMap.values()).map((finding) => {
|
|
65
|
+
let level: 'error' | 'warning' | 'note' = 'warning';
|
|
66
|
+
if (finding.severity === 'CRITICAL' || finding.severity === 'HIGH') {
|
|
67
|
+
level = 'error';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
id: finding.ruleId,
|
|
72
|
+
name: finding.ruleId,
|
|
73
|
+
shortDescription: {
|
|
74
|
+
text:
|
|
75
|
+
finding.detailedExplanation?.summary ||
|
|
76
|
+
`Vulnerability detected: ${finding.ruleId}`,
|
|
77
|
+
},
|
|
78
|
+
fullDescription: {
|
|
79
|
+
text:
|
|
80
|
+
finding.detailedExplanation?.impact ||
|
|
81
|
+
`Reachable issue found for ${finding.ruleId}.`,
|
|
82
|
+
},
|
|
83
|
+
defaultConfiguration: { level },
|
|
84
|
+
help: {
|
|
85
|
+
text:
|
|
86
|
+
finding.detailedExplanation?.remediationGuide ||
|
|
87
|
+
'Remediate this issue using warden fix.',
|
|
88
|
+
markdown: `**Impact:** ${finding.detailedExplanation?.impact || 'High'}\n\n**Remediation:** ${
|
|
89
|
+
finding.detailedExplanation?.remediationGuide ||
|
|
90
|
+
'Move secrets to environment variables or parameterize queries.'
|
|
91
|
+
}`,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const sarifResults = findings.map((finding) => {
|
|
97
|
+
let level: 'error' | 'warning' | 'note' = 'warning'
|
|
98
|
+
if (finding.severity === 'CRITICAL' || finding.severity === 'HIGH') {
|
|
99
|
+
level = 'error'
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let relativeUri = finding.location.file
|
|
103
|
+
if (relativeUri.startsWith(rootDir)) {
|
|
104
|
+
relativeUri = relativeUri.replace(rootDir, '').replace(/^[/\\]/, '')
|
|
105
|
+
}
|
|
106
|
+
relativeUri = relativeUri.replace(/\\/g, '/')
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
ruleId: finding.ruleId,
|
|
110
|
+
level,
|
|
111
|
+
message: {
|
|
112
|
+
text: finding.detailedExplanation?.summary || `Reachable ${finding.ruleId} vulnerability.`
|
|
113
|
+
},
|
|
114
|
+
locations: [
|
|
115
|
+
{
|
|
116
|
+
physicalLocation: {
|
|
117
|
+
artifactLocation: { uri: relativeUri },
|
|
118
|
+
region: {
|
|
119
|
+
startLine: finding.location.line || 1,
|
|
120
|
+
startColumn: finding.location.column || 1,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
]
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
130
|
+
version: '2.1.0',
|
|
131
|
+
runs: [
|
|
132
|
+
{
|
|
133
|
+
tool: {
|
|
134
|
+
driver: {
|
|
135
|
+
name: 'Warden AST Engine',
|
|
136
|
+
version: '1.0.0',
|
|
137
|
+
informationUri: 'https://github.com/sectorzero-hq/warden',
|
|
138
|
+
rules: sarifRules,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
results: sarifResults
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"]
|
|
16
|
+
}
|
package/tsup.config.ts
ADDED
package/README.md
DELETED