@activemind/scd 1.4.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/LICENSE.md +35 -0
- package/README.md +417 -0
- package/bin/scd.js +140 -0
- package/lib/audit-report.js +93 -0
- package/lib/audit-sync.js +172 -0
- package/lib/audit.js +356 -0
- package/lib/cli-helpers.js +108 -0
- package/lib/commands/accept.js +28 -0
- package/lib/commands/audit.js +17 -0
- package/lib/commands/configure.js +200 -0
- package/lib/commands/doctor.js +14 -0
- package/lib/commands/exceptions.js +19 -0
- package/lib/commands/export-findings.js +46 -0
- package/lib/commands/findings.js +306 -0
- package/lib/commands/ignore.js +28 -0
- package/lib/commands/init.js +16 -0
- package/lib/commands/insights.js +24 -0
- package/lib/commands/install.js +15 -0
- package/lib/commands/list.js +109 -0
- package/lib/commands/remove.js +16 -0
- package/lib/commands/repo.js +862 -0
- package/lib/commands/report.js +234 -0
- package/lib/commands/resolve.js +25 -0
- package/lib/commands/rules.js +185 -0
- package/lib/commands/scan.js +519 -0
- package/lib/commands/scope.js +341 -0
- package/lib/commands/sync.js +40 -0
- package/lib/commands/uninstall.js +15 -0
- package/lib/commands/version.js +33 -0
- package/lib/comment-map.js +388 -0
- package/lib/config.js +325 -0
- package/lib/context-modifiers.js +211 -0
- package/lib/deep-analyzer.js +225 -0
- package/lib/doctor.js +236 -0
- package/lib/exception-manager.js +675 -0
- package/lib/export-findings.js +376 -0
- package/lib/file-context.js +380 -0
- package/lib/file-filter.js +204 -0
- package/lib/file-manifest.js +145 -0
- package/lib/git-utils.js +102 -0
- package/lib/global-config.js +239 -0
- package/lib/hooks-manager.js +130 -0
- package/lib/init-repo.js +147 -0
- package/lib/insights-analyzer.js +416 -0
- package/lib/insights-output.js +160 -0
- package/lib/installer.js +128 -0
- package/lib/output-constants.js +32 -0
- package/lib/output-terminal.js +407 -0
- package/lib/push-queue.js +322 -0
- package/lib/remove-repo.js +108 -0
- package/lib/repo-context.js +187 -0
- package/lib/report-html.js +1154 -0
- package/lib/report-index.js +157 -0
- package/lib/report-json.js +136 -0
- package/lib/report-markdown.js +250 -0
- package/lib/resolve-manager.js +148 -0
- package/lib/rule-registry.js +205 -0
- package/lib/scan-cache.js +171 -0
- package/lib/scan-context.js +312 -0
- package/lib/scan-schema.js +67 -0
- package/lib/scanner-full.js +681 -0
- package/lib/scanner-manual.js +348 -0
- package/lib/scanner-secrets.js +83 -0
- package/lib/scope.js +331 -0
- package/lib/store-verify.js +395 -0
- package/lib/store.js +310 -0
- package/lib/taint-register.js +196 -0
- package/lib/version-check.js +46 -0
- package/package.json +37 -0
- package/rules/rule-loader.js +324 -0
- package/rules/rules-aspx-cs.json +399 -0
- package/rules/rules-aspx.json +222 -0
- package/rules/rules-infra-leakage.json +434 -0
- package/rules/rules-js.json +664 -0
- package/rules/rules-php.json +521 -0
- package/rules/rules-python.json +466 -0
- package/rules/rules-secrets.json +99 -0
- package/rules/rules-sensitive-files.json +475 -0
- package/rules/rules-ts.json +76 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { RESET, BOLD, DIM, RED, GREEN, YELLOW, CYAN } = require('../output-constants');
|
|
3
|
+
// lib/commands/scope.js
|
|
4
|
+
// scd scope — manage global scan scope exclusions (~/.scd/scope.yml)
|
|
5
|
+
// For repo-level scope, use: scd repo scope
|
|
6
|
+
|
|
7
|
+
module.exports = { register, appendToScope, buildFileEntry, buildRuleEntry, removeFromScope };
|
|
8
|
+
|
|
9
|
+
function register(program) {
|
|
10
|
+
const { Command } = require('commander');
|
|
11
|
+
|
|
12
|
+
const scopeCmd = new Command('scope')
|
|
13
|
+
.description('Manage global scan scope exclusions (~/.scd/scope.yml)')
|
|
14
|
+
.addHelpText('after', `
|
|
15
|
+
Examples:
|
|
16
|
+
scd scope --show
|
|
17
|
+
scd scope --add-file "tests/fixtures/" --reason "Test fixtures with intentional vulns"
|
|
18
|
+
scd scope --add-rule INFRA-001 --reason "Cloud-managed infrastructure"
|
|
19
|
+
scd scope --add-rule JS-ERR-002 --files "lib/rules/,**/*.test.js" --reason "Rule definition files"
|
|
20
|
+
|
|
21
|
+
For repo-level scope: scd repo scope --show`)
|
|
22
|
+
.option('--show', 'Show active global scope exclusions')
|
|
23
|
+
.option('--add-file <pattern>','Add a file/directory exclusion pattern')
|
|
24
|
+
.option('--add-rule <ruleId>', 'Add a rule exclusion')
|
|
25
|
+
.option('--files <globs>', 'Comma-separated file globs to scope a rule exclusion (use with --add-rule)')
|
|
26
|
+
.option('--reason <text>', 'Reason for the exclusion (required with --add-file and --add-rule)')
|
|
27
|
+
.option('--remove-file <pattern>','Remove a file exclusion by pattern')
|
|
28
|
+
.option('--remove-rule <ruleId>', 'Remove a rule exclusion by rule ID')
|
|
29
|
+
.action((opts) => {
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const os = require('os');
|
|
32
|
+
const path = require('path');
|
|
33
|
+
const store = require('../store');
|
|
34
|
+
const { loadScope, validateScope, summariseScope } = require('../scope');
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
const scopeFile = store.globalScopePath();
|
|
38
|
+
|
|
39
|
+
// ── --show ──────────────────────────────────────────────────────────────
|
|
40
|
+
if (opts.show || (!opts.addFile && !opts.addRule && !opts.removeFile && !opts.removeRule)) {
|
|
41
|
+
if (!fs.existsSync(scopeFile)) {
|
|
42
|
+
console.log(`\n${DIM} No global scope.yml found.${RESET}`);
|
|
43
|
+
console.log(`${DIM} Use ${RESET}${CYAN}scd scope --add-file${RESET}${DIM} or ${RESET}${CYAN}scd scope --add-rule${RESET}${DIM} to create one.${RESET}\n`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const scope = loadScope(null); // global only — no repoRoot
|
|
48
|
+
const warnings = validateScope(scope);
|
|
49
|
+
const summary = summariseScope(scope);
|
|
50
|
+
|
|
51
|
+
console.log(`\n${BOLD}Global scope exclusions${RESET} ${DIM}(~/.scd/scope.yml)${RESET}\n`);
|
|
52
|
+
|
|
53
|
+
if (!summary.hasExclusions) {
|
|
54
|
+
console.log(`${DIM} No active exclusions.${RESET}\n`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (summary.fileLines.length > 0) {
|
|
59
|
+
console.log(`${BOLD} File exclusions:${RESET}`);
|
|
60
|
+
for (const line of summary.fileLines) console.log(` ${line.trim()}`);
|
|
61
|
+
console.log();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (summary.ruleLines.length > 0) {
|
|
65
|
+
console.log(`${BOLD} Rule exclusions:${RESET}`);
|
|
66
|
+
for (const line of summary.ruleLines) console.log(` ${line.trim()}`);
|
|
67
|
+
console.log();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (warnings.length > 0) {
|
|
71
|
+
console.log(`${YELLOW} ⚠ Incomplete entries (missing required fields):${RESET}`);
|
|
72
|
+
for (const w of warnings) {
|
|
73
|
+
console.log(`${YELLOW} ${w.identifier}: missing ${w.missing.join(', ')}${RESET}`);
|
|
74
|
+
}
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── --add-file / --add-rule: require --reason ────────────────────────────
|
|
81
|
+
if (!opts.reason) {
|
|
82
|
+
console.error(`\n${RED}✗ --reason is required.${RESET}`);
|
|
83
|
+
console.error(` Every scope exclusion must have a documented reason.\n`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Build entry ──────────────────────────────────────────────────────────
|
|
88
|
+
const { getMachineFingerprint } = require('../store');
|
|
89
|
+
const installId = getMachineFingerprint() || 'unknown';
|
|
90
|
+
const addedAt = new Date().toLocaleString('sv-SE', {
|
|
91
|
+
timeZone: 'Europe/Stockholm', year: 'numeric', month: '2-digit',
|
|
92
|
+
day: '2-digit', hour: '2-digit', minute: '2-digit',
|
|
93
|
+
}).replace(',', '');
|
|
94
|
+
|
|
95
|
+
// ── --add-file ───────────────────────────────────────────────────────────
|
|
96
|
+
if (opts.addFile) {
|
|
97
|
+
const entry = buildFileEntry(opts.addFile, opts.reason, installId, addedAt);
|
|
98
|
+
appendToScope(scopeFile, 'file_excludes', entry);
|
|
99
|
+
console.log(`\n${GREEN}✓ File exclusion added to global scope.yml${RESET}`);
|
|
100
|
+
console.log(` ${DIM}Pattern : ${opts.addFile}${RESET}`);
|
|
101
|
+
console.log(` ${DIM}Reason : ${opts.reason}${RESET}`);
|
|
102
|
+
console.log(` ${DIM}Added by: ${installId}${RESET}\n`);
|
|
103
|
+
console.log(`${YELLOW} ⚠ Active file exclusions are visible in every scan output.${RESET}\n`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── --add-rule ───────────────────────────────────────────────────────────
|
|
108
|
+
if (opts.addRule) {
|
|
109
|
+
const files = opts.files
|
|
110
|
+
? opts.files.split(',').map(s => s.trim()).filter(Boolean)
|
|
111
|
+
: null;
|
|
112
|
+
const entry = buildRuleEntry(opts.addRule, files, opts.reason, installId, addedAt);
|
|
113
|
+
appendToScope(scopeFile, 'rule_excludes', entry);
|
|
114
|
+
const scopeDesc = files ? files.join(', ') : 'all files';
|
|
115
|
+
console.log(`\n${GREEN}✓ Rule exclusion added to global scope.yml${RESET}`);
|
|
116
|
+
console.log(` ${DIM}Rule : ${opts.addRule}${RESET}`);
|
|
117
|
+
console.log(` ${DIM}Scope : ${scopeDesc}${RESET}`);
|
|
118
|
+
console.log(` ${DIM}Reason : ${opts.reason}${RESET}`);
|
|
119
|
+
console.log(` ${DIM}Added by: ${installId}${RESET}\n`);
|
|
120
|
+
console.log(`${YELLOW} ⚠ Active rule exclusions are visible in every scan output.${RESET}\n`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── --remove-file ─────────────────────────────────────────────────────────
|
|
125
|
+
if (opts.removeFile) {
|
|
126
|
+
const removed = removeFromScope(scopeFile, 'file_excludes', 'pattern', opts.removeFile);
|
|
127
|
+
if (removed.length === 0) {
|
|
128
|
+
console.log(`\n${YELLOW} No file exclusion found matching: ${opts.removeFile}${RESET}\n`);
|
|
129
|
+
} else {
|
|
130
|
+
console.log(`\n${GREEN}✓ Removed ${removed.length} file exclusion(s) from global scope.yml${RESET}`);
|
|
131
|
+
for (const r of removed) {
|
|
132
|
+
console.log(` ${DIM}Pattern : ${r.pattern}${RESET}`);
|
|
133
|
+
console.log(` ${DIM}Reason : ${r.reason || '(none)'}${RESET}`);
|
|
134
|
+
console.log(` ${DIM}Added by: ${r.added_by || '(unknown)'} ${r.added_at || ''}${RESET}`);
|
|
135
|
+
}
|
|
136
|
+
console.log();
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── --remove-rule ─────────────────────────────────────────────────────────
|
|
142
|
+
if (opts.removeRule) {
|
|
143
|
+
const removed = removeFromScope(scopeFile, 'rule_excludes', 'rule', opts.removeRule);
|
|
144
|
+
if (removed.length === 0) {
|
|
145
|
+
console.log(`\n${YELLOW} No rule exclusion found matching: ${opts.removeRule}${RESET}\n`);
|
|
146
|
+
} else {
|
|
147
|
+
console.log(`\n${GREEN}✓ Removed ${removed.length} rule exclusion(s) from global scope.yml${RESET}`);
|
|
148
|
+
for (const r of removed) {
|
|
149
|
+
const scopeDesc = r.files && r.files.length ? r.files.join(', ') : 'all files';
|
|
150
|
+
console.log(` ${DIM}Rule : ${r.rule} (${scopeDesc})${RESET}`);
|
|
151
|
+
console.log(` ${DIM}Reason : ${r.reason || '(none)'}${RESET}`);
|
|
152
|
+
console.log(` ${DIM}Added by: ${r.added_by || '(unknown)'} ${r.added_at || ''}${RESET}`);
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
program.addCommand(scopeCmd);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
function buildFileEntry(pattern, reason, addedBy, addedAt) {
|
|
166
|
+
return [
|
|
167
|
+
` - pattern: ${pattern}`,
|
|
168
|
+
` reason: "${reason}"`,
|
|
169
|
+
` added_by: ${addedBy}`,
|
|
170
|
+
` added_at: "${addedAt}"`,
|
|
171
|
+
].join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildRuleEntry(rule, files, reason, addedBy, addedAt) {
|
|
175
|
+
const lines = [
|
|
176
|
+
` - rule: ${rule}`,
|
|
177
|
+
];
|
|
178
|
+
if (files && files.length) {
|
|
179
|
+
lines.push(` files:`);
|
|
180
|
+
for (const f of files) lines.push(` - ${f}`);
|
|
181
|
+
}
|
|
182
|
+
lines.push(` reason: "${reason}"`);
|
|
183
|
+
lines.push(` added_by: ${addedBy}`);
|
|
184
|
+
lines.push(` added_at: "${addedAt}"`);
|
|
185
|
+
return lines.join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Remove all entries from a section in scope.yml that match a key/value.
|
|
191
|
+
* Parses the YAML as text — uses the scope parser to identify entries,
|
|
192
|
+
* then removes their line ranges from the raw file.
|
|
193
|
+
*
|
|
194
|
+
* Returns an array of removed entries (for display).
|
|
195
|
+
*/
|
|
196
|
+
function removeFromScope(scopeFile, section, matchKey, matchValue) {
|
|
197
|
+
const fs = require('fs');
|
|
198
|
+
const { parseScope } = require('../scope');
|
|
199
|
+
|
|
200
|
+
if (!fs.existsSync(scopeFile)) return [];
|
|
201
|
+
|
|
202
|
+
const content = fs.readFileSync(scopeFile, 'utf8');
|
|
203
|
+
const parsed = parseScope(content);
|
|
204
|
+
const entries = parsed[section] || [];
|
|
205
|
+
|
|
206
|
+
// Find entries that match
|
|
207
|
+
const toRemove = entries.filter(e => e[matchKey] === matchValue);
|
|
208
|
+
if (toRemove.length === 0) return [];
|
|
209
|
+
|
|
210
|
+
// Remove by rebuilding the section without matched entries
|
|
211
|
+
const remaining = entries.filter(e => e[matchKey] !== matchValue);
|
|
212
|
+
|
|
213
|
+
// Rebuild file: replace section content
|
|
214
|
+
const lines = content.split('\n');
|
|
215
|
+
const newLines = [];
|
|
216
|
+
let inSection = false;
|
|
217
|
+
let inEntry = false;
|
|
218
|
+
let skipEntry = false;
|
|
219
|
+
let entryLines = [];
|
|
220
|
+
|
|
221
|
+
// Two-pass: collect entry line ranges, then rebuild
|
|
222
|
+
// Simpler approach: regenerate the section from remaining entries
|
|
223
|
+
const otherSection = section === 'file_excludes' ? 'rule_excludes' : 'file_excludes';
|
|
224
|
+
const otherEntries = parsed[otherSection] || [];
|
|
225
|
+
|
|
226
|
+
let result = rebuildScopeFile(content, section, remaining, otherSection, otherEntries);
|
|
227
|
+
fs.writeFileSync(scopeFile, result, { encoding: 'utf8', mode: 0o600 });
|
|
228
|
+
|
|
229
|
+
return toRemove;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Rebuild scope.yml content preserving header comments and both sections.
|
|
234
|
+
* Regenerates section entries from parsed data.
|
|
235
|
+
*/
|
|
236
|
+
function rebuildScopeFile(original, changedSection, changedEntries, otherSection, otherEntries) {
|
|
237
|
+
const lines = original.split('\n');
|
|
238
|
+
const result = [];
|
|
239
|
+
let inSection = null;
|
|
240
|
+
let skipUntilNextSection = false;
|
|
241
|
+
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
// Detect top-level section headers
|
|
244
|
+
if (/^file_excludes\s*:/.test(line)) {
|
|
245
|
+
inSection = 'file_excludes';
|
|
246
|
+
skipUntilNextSection = true;
|
|
247
|
+
result.push(line);
|
|
248
|
+
// Inject entries for this section
|
|
249
|
+
const entries = inSection === changedSection ? changedEntries : otherEntries;
|
|
250
|
+
for (const e of (changedSection === 'file_excludes' ? changedEntries : otherEntries)) {
|
|
251
|
+
result.push(buildFileEntry(e.pattern, e.reason || '', e.added_by || '', e.added_at || ''));
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (/^rule_excludes\s*:/.test(line)) {
|
|
256
|
+
inSection = 'rule_excludes';
|
|
257
|
+
skipUntilNextSection = true;
|
|
258
|
+
result.push(line);
|
|
259
|
+
for (const e of (changedSection === 'rule_excludes' ? changedEntries : otherEntries)) {
|
|
260
|
+
result.push(buildRuleEntry(e.rule, e.files || null, e.reason || '', e.added_by || '', e.added_at || ''));
|
|
261
|
+
}
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Skip existing entry lines inside a section
|
|
266
|
+
if (skipUntilNextSection && inSection) {
|
|
267
|
+
// A line that is not indented and not empty and not a comment = new top-level key
|
|
268
|
+
if (line.length > 0 && !line.startsWith(' ') && !line.startsWith('\t') && !line.startsWith('#')) {
|
|
269
|
+
skipUntilNextSection = false;
|
|
270
|
+
inSection = null;
|
|
271
|
+
result.push(line);
|
|
272
|
+
}
|
|
273
|
+
// Otherwise skip — entries were already injected above
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
result.push(line);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return result.join('\n');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Append a new entry to the correct section in scope.yml.
|
|
285
|
+
* Creates the file with header if it does not exist.
|
|
286
|
+
* Appends to existing section if present, adds section header if not.
|
|
287
|
+
*/
|
|
288
|
+
function appendToScope(scopeFile, section, entryYaml) {
|
|
289
|
+
const fs = require('fs');
|
|
290
|
+
const path = require('path');
|
|
291
|
+
const os = require('os');
|
|
292
|
+
|
|
293
|
+
// Ensure ~/.scd/ exists
|
|
294
|
+
const dir = path.dirname(scopeFile);
|
|
295
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
296
|
+
|
|
297
|
+
if (!fs.existsSync(scopeFile)) {
|
|
298
|
+
// Create file with header and both section stubs
|
|
299
|
+
const header = [
|
|
300
|
+
`# scope.yml — global scan scope exclusions`,
|
|
301
|
+
`# Managed by: scd scope`,
|
|
302
|
+
`# Every entry requires reason, added_by, and added_at.`,
|
|
303
|
+
`# Missing fields produce a warning in scan output and audit log.`,
|
|
304
|
+
`#`,
|
|
305
|
+
`# Documentation: https://docs.securecodebydesign.com/scope`,
|
|
306
|
+
``,
|
|
307
|
+
`file_excludes:`,
|
|
308
|
+
section === 'file_excludes' ? entryYaml : '',
|
|
309
|
+
``,
|
|
310
|
+
`rule_excludes:`,
|
|
311
|
+
section === 'rule_excludes' ? entryYaml : '',
|
|
312
|
+
``,
|
|
313
|
+
].join('\n');
|
|
314
|
+
fs.writeFileSync(scopeFile, header, { encoding: 'utf8', mode: 0o600 });
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
let content = fs.readFileSync(scopeFile, 'utf8');
|
|
319
|
+
|
|
320
|
+
// Section already exists — append after the section header
|
|
321
|
+
const sectionRe = new RegExp(`^(${section}\\s*:[ \\t]*)$`, 'm');
|
|
322
|
+
if (sectionRe.test(content)) {
|
|
323
|
+
// Find the section and append entry before the next top-level key or EOF
|
|
324
|
+
const lines = content.split('\n');
|
|
325
|
+
const sectionIdx = lines.findIndex(l => new RegExp(`^${section}\\s*:`).test(l));
|
|
326
|
+
// Find end of section: next line that is a top-level key (no leading space) and not a comment
|
|
327
|
+
let insertIdx = sectionIdx + 1;
|
|
328
|
+
while (insertIdx < lines.length) {
|
|
329
|
+
const line = lines[insertIdx];
|
|
330
|
+
if (line.length > 0 && !line.startsWith(' ') && !line.startsWith('\t') && !line.startsWith('#')) break;
|
|
331
|
+
insertIdx++;
|
|
332
|
+
}
|
|
333
|
+
lines.splice(insertIdx, 0, entryYaml);
|
|
334
|
+
content = lines.join('\n');
|
|
335
|
+
} else {
|
|
336
|
+
// Section does not exist — append to end of file
|
|
337
|
+
content = content.trimEnd() + `\n\n${section}:\n${entryYaml}\n`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fs.writeFileSync(scopeFile, content, { encoding: 'utf8', mode: 0o600 });
|
|
341
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { RESET, DIM, GREEN, CYAN } = require('../output-constants');
|
|
3
|
+
// lib/commands/sync.js
|
|
4
|
+
|
|
5
|
+
module.exports = { register };
|
|
6
|
+
|
|
7
|
+
function register(program) {
|
|
8
|
+
program
|
|
9
|
+
.command('sync')
|
|
10
|
+
.description('Pull approved exceptions from scd-server and update local config')
|
|
11
|
+
.option('--history', 'Sync full audit.log history to scd-server (one-time, idempotent)')
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const { getRepoRoot } = require('../config');
|
|
14
|
+
const { warnIfOutdated } = require('../cli-helpers');
|
|
15
|
+
const repoRoot = getRepoRoot();
|
|
16
|
+
|
|
17
|
+
if (opts.history) {
|
|
18
|
+
const { syncHistory } = require('../audit-sync');
|
|
19
|
+
console.log('\n' + DIM + ' Syncing audit history to scd-server…' + RESET);
|
|
20
|
+
const result = await syncHistory(repoRoot);
|
|
21
|
+
if (result.error) {
|
|
22
|
+
console.log('\nRED Error: ' + result.error + RESET + '\n');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (result.message) {
|
|
26
|
+
console.log('\n' + DIM + ' ' + result.message + RESET + '\n');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log('\n' + GREEN + ' ✓ History sync complete' + RESET);
|
|
30
|
+
console.log(DIM + ' Sessions: ' + result.sessions + ' · Findings: ' + result.findings +
|
|
31
|
+
(result.errors > 0 ? ' · ' + YELLOW + 'Errors: ' + result.errors + RESET : '') + '\n' + RESET);
|
|
32
|
+
console.log(DIM + ' Safe to re-run — server ignores duplicates.\n' + RESET);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { syncExceptions } = require('../exception-manager');
|
|
37
|
+
await syncExceptions(repoRoot);
|
|
38
|
+
warnIfOutdated();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// lib/commands/uninstall.js
|
|
3
|
+
|
|
4
|
+
module.exports = { register };
|
|
5
|
+
|
|
6
|
+
function register(program) {
|
|
7
|
+
const { Command } = require('commander');
|
|
8
|
+
const cmd = new Command('uninstall')
|
|
9
|
+
.description('Remove global git hooks from this machine')
|
|
10
|
+
.action(async () => {
|
|
11
|
+
const { uninstall } = require('../installer');
|
|
12
|
+
await uninstall();
|
|
13
|
+
});
|
|
14
|
+
program.addCommand(cmd);
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { RESET, BOLD, DIM } = require('../output-constants');
|
|
3
|
+
// lib/commands/version.js
|
|
4
|
+
|
|
5
|
+
module.exports = { register, showVersion };
|
|
6
|
+
|
|
7
|
+
function showVersion() {
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const pkg = require('../../package.json');
|
|
10
|
+
const { RULES_VERSION, getRegistry } = require('../rule-registry');
|
|
11
|
+
const rules = getRegistry();
|
|
12
|
+
const sevCount = (sev) => rules.filter(r => r.severity === sev).length;
|
|
13
|
+
|
|
14
|
+
console.log('\n' + BOLD + 'Secure Code by Design' + RESET);
|
|
15
|
+
console.log(DIM + '─'.repeat(40) + RESET);
|
|
16
|
+
console.log(' CLI: ' + BOLD + pkg.version + RESET);
|
|
17
|
+
console.log(' Rules: ' + BOLD + RULES_VERSION + RESET +
|
|
18
|
+
DIM + ' (' + rules.length + ' rules' +
|
|
19
|
+
' · CRITICAL: ' + sevCount('CRITICAL') +
|
|
20
|
+
' HIGH: ' + sevCount('HIGH') +
|
|
21
|
+
' MEDIUM: ' + sevCount('MEDIUM') +
|
|
22
|
+
' EXPOSURE: ' + sevCount('EXPOSURE') + ')' + RESET);
|
|
23
|
+
console.log(' Node: ' + DIM + process.version + RESET);
|
|
24
|
+
console.log(' OS: ' + DIM + os.platform() + ' ' + os.arch() + RESET);
|
|
25
|
+
console.log();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function register(program) {
|
|
29
|
+
program
|
|
30
|
+
.command('version')
|
|
31
|
+
.description('Show detailed version information')
|
|
32
|
+
.action(showVersion);
|
|
33
|
+
}
|