@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,862 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { RESET, BOLD, DIM, RED, GREEN, YELLOW, CYAN, OK } = require('../output-constants');
|
|
3
|
+
// lib/commands/repo.js
|
|
4
|
+
|
|
5
|
+
module.exports = { register };
|
|
6
|
+
|
|
7
|
+
function register(program) {
|
|
8
|
+
const { Command } = require('commander');
|
|
9
|
+
const { getRepoRoot } = require('../config');
|
|
10
|
+
|
|
11
|
+
// ── scd repo configure ────────────────────────────────────────────────────
|
|
12
|
+
const repoConfigureCmd = new Command('configure')
|
|
13
|
+
.description('Show and manage per-repo configuration')
|
|
14
|
+
.option('--show', 'Show current effective configuration (default)')
|
|
15
|
+
.option('--trust-level <value>', 'Set trust level (maximum_privacy|balanced|maximum_analysis)')
|
|
16
|
+
.option('--scan-mode <value>', 'Set scan mode (full|fast)')
|
|
17
|
+
.option('--block-on-high <value>', 'Set block-on-high (true|false)')
|
|
18
|
+
.option('--block-on-critical <value>', 'Set block-on-critical (true|false)')
|
|
19
|
+
.action((opts) => {
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const store = require('../store');
|
|
22
|
+
const yaml = require('../config');
|
|
23
|
+
const gc = require('../global-config');
|
|
24
|
+
const repoRoot = getRepoRoot();
|
|
25
|
+
const configPath = store.configPath(repoRoot);
|
|
26
|
+
|
|
27
|
+
const VALID_TRUST = ['maximum_privacy', 'balanced', 'maximum_analysis'];
|
|
28
|
+
const VALID_MODES = ['full', 'fast'];
|
|
29
|
+
const KEYS = ['trust_level', 'scan_mode', 'block_on_critical', 'block_on_high'];
|
|
30
|
+
|
|
31
|
+
// Helper: read config.yml, update a key value in-place, write back
|
|
32
|
+
function updateConfigYml(key, value) {
|
|
33
|
+
if (!fs.existsSync(configPath)) {
|
|
34
|
+
console.error(`\n${RED}✗ No config.yml found for this repo.${RESET}`);
|
|
35
|
+
console.error(` Run ${CYAN}scd init${RESET} first.\n`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
let content = fs.readFileSync(configPath, 'utf8');
|
|
39
|
+
const re = new RegExp(`^(${key}:\\s*).*$`, 'm');
|
|
40
|
+
if (re.test(content)) {
|
|
41
|
+
content = content.replace(re, `$1${value}`);
|
|
42
|
+
} else {
|
|
43
|
+
content = content.trimEnd() + `\n${key}: ${value}\n`;
|
|
44
|
+
}
|
|
45
|
+
fs.writeFileSync(configPath, content, 'utf8');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── set operations ──────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
if (opts.trustLevel !== undefined) {
|
|
51
|
+
if (!VALID_TRUST.includes(opts.trustLevel)) {
|
|
52
|
+
console.error(`\n${RED}✗ Invalid trust level. Use: ${VALID_TRUST.join(' | ')}${RESET}\n`);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
updateConfigYml('trust_level', opts.trustLevel);
|
|
56
|
+
console.log(`\n${GREEN}✓ trust_level set to ${opts.trustLevel}${RESET} for this repo\n`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (opts.scanMode !== undefined) {
|
|
61
|
+
if (!VALID_MODES.includes(opts.scanMode)) {
|
|
62
|
+
console.error(`\n${RED}✗ Invalid scan mode. Use: full | fast${RESET}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
updateConfigYml('scan_mode', opts.scanMode);
|
|
66
|
+
console.log(`\n${GREEN}✓ scan_mode set to ${opts.scanMode}${RESET} for this repo\n`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (opts.blockOnHigh !== undefined) {
|
|
71
|
+
const val = opts.blockOnHigh.toLowerCase();
|
|
72
|
+
if (val !== 'true' && val !== 'false') {
|
|
73
|
+
console.error(`\n${RED}✗ Invalid value. Use: true | false${RESET}\n`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
updateConfigYml('block_on_high', val);
|
|
77
|
+
console.log(`\n${GREEN}✓ block_on_high set to ${val}${RESET} for this repo\n`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (opts.blockOnCritical !== undefined) {
|
|
82
|
+
const val = opts.blockOnCritical.toLowerCase();
|
|
83
|
+
if (val !== 'true' && val !== 'false') {
|
|
84
|
+
console.error(`\n${RED}✗ Invalid value. Use: true | false${RESET}\n`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
updateConfigYml('block_on_critical', val);
|
|
88
|
+
console.log(`\n${GREEN}✓ block_on_critical set to ${val}${RESET} for this repo\n`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── show (default) ──────────────────────────────────────────────────
|
|
93
|
+
const config = yaml.loadConfig(repoRoot);
|
|
94
|
+
|
|
95
|
+
let repoYaml = {};
|
|
96
|
+
if (fs.existsSync(configPath)) {
|
|
97
|
+
try {
|
|
98
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
99
|
+
for (const key of KEYS) {
|
|
100
|
+
const m = raw.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
101
|
+
if (m) repoYaml[key] = m[1].trim();
|
|
102
|
+
}
|
|
103
|
+
} catch { /* ignore */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log(`\n${CYAN}${BOLD}Secure Code by Design – Repo configuration${RESET}`);
|
|
107
|
+
console.log(`${DIM}${'─'.repeat(52)}${RESET}`);
|
|
108
|
+
console.log(`${DIM}Repo: ${repoRoot}${RESET}`);
|
|
109
|
+
console.log(`${DIM}Config: ${configPath}${RESET}\n`);
|
|
110
|
+
console.log(` ${'Setting'.padEnd(22)}${'Value'.padEnd(22)}Source`);
|
|
111
|
+
console.log(` ${DIM}${'─'.repeat(54)}${RESET}`);
|
|
112
|
+
|
|
113
|
+
for (const key of KEYS) {
|
|
114
|
+
const inRepo = key in repoYaml;
|
|
115
|
+
const globalRaw = gc.get('REPO_' + key.toUpperCase());
|
|
116
|
+
const inGlobal = globalRaw !== undefined;
|
|
117
|
+
const val = String(config[key]);
|
|
118
|
+
const source = inRepo ? `${GREEN}repo${RESET}`
|
|
119
|
+
: inGlobal ? `${CYAN}global${RESET}`
|
|
120
|
+
: `${DIM}default${RESET}`;
|
|
121
|
+
console.log(` ${DIM}${key.padEnd(22)}${RESET}${val.padEnd(22)}${source}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log(` ${DIM}scd repo configure --scan-mode <fast|full> set for this repo${RESET}`);
|
|
126
|
+
console.log(` ${DIM}scd repo configure --trust-level <value> set for this repo${RESET}`);
|
|
127
|
+
console.log(` ${DIM}scd repo configure --block-on-high <true|false> set for this repo${RESET}`);
|
|
128
|
+
console.log(` ${DIM}scd configure --scan-mode <value> set global default${RESET}`);
|
|
129
|
+
console.log('');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
// ── scd repo hooks ────────────────────────────────────────────────────────
|
|
134
|
+
const repoHooksCmd = new Command('hooks')
|
|
135
|
+
.description('Show or manage git hook status for the current repo')
|
|
136
|
+
.option('--disable', 'Disable git hooks for this repo (requires --reason)')
|
|
137
|
+
.option('--enable', 'Re-enable git hooks for this repo')
|
|
138
|
+
.option('--reason <text>', 'Required reason when disabling hooks (logged to audit trail)')
|
|
139
|
+
.action((opts) => {
|
|
140
|
+
const { getHookStatus, disableHooks, enableHooks } = require('../hooks-manager');
|
|
141
|
+
const { logHooks } = require('../audit');
|
|
142
|
+
const repoRoot = getRepoRoot();
|
|
143
|
+
|
|
144
|
+
// ── disable ────────────────────────────────────────────────────────
|
|
145
|
+
if (opts.disable) {
|
|
146
|
+
if (!opts.reason || opts.reason.trim().length < 5) {
|
|
147
|
+
console.error(`\n${RED}✗ --reason is required when disabling hooks.${RESET}`);
|
|
148
|
+
console.error(` Example: scd repo hooks --disable --reason "demo repo, no real secrets"\n`);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const current = getHookStatus(repoRoot);
|
|
152
|
+
if (current.status === 'disabled') {
|
|
153
|
+
console.log(`\n${YELLOW}⚠ Hooks are already disabled for this repo.${RESET}\n`);
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
disableHooks(repoRoot, opts.reason);
|
|
158
|
+
logHooks(repoRoot, { action: 'disable', reason: opts.reason.trim(), noSync: false });
|
|
159
|
+
console.log(`\n${YELLOW}⚠ Git hooks disabled for this repo.${RESET}`);
|
|
160
|
+
console.log(` ${DIM}Reason: ${opts.reason.trim()}${RESET}`);
|
|
161
|
+
console.log(` ${DIM}This action has been logged to the audit trail.${RESET}`);
|
|
162
|
+
console.log(` ${DIM}Re-enable with: scd repo hooks --enable${RESET}\n`);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error(`\n${RED}✗ Failed to disable hooks: ${err.message}${RESET}\n`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── enable ─────────────────────────────────────────────────────────
|
|
171
|
+
if (opts.enable) {
|
|
172
|
+
const current = getHookStatus(repoRoot);
|
|
173
|
+
if (current.status === 'enabled' && current.source !== 'local') {
|
|
174
|
+
console.log(`\n${GREEN}✓ Hooks are already enabled (via global config).${RESET}\n`);
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
enableHooks(repoRoot);
|
|
179
|
+
logHooks(repoRoot, { action: 'enable', reason: 'manually re-enabled', noSync: false });
|
|
180
|
+
console.log(`\n${GREEN}✓ Git hooks re-enabled for this repo.${RESET}`);
|
|
181
|
+
console.log(` ${DIM}This action has been logged to the audit trail.${RESET}\n`);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.error(`\n${RED}✗ Failed to enable hooks: ${err.message}${RESET}\n`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── show (default) ─────────────────────────────────────────────────
|
|
190
|
+
const status = getHookStatus(repoRoot);
|
|
191
|
+
console.log(`\n${CYAN}${BOLD}Git hook status${RESET}`);
|
|
192
|
+
console.log(`${DIM}${'─'.repeat(40)}${RESET}`);
|
|
193
|
+
console.log(`${DIM}Repo: ${repoRoot}${RESET}\n`);
|
|
194
|
+
|
|
195
|
+
const statusLabel = status.status === 'enabled'
|
|
196
|
+
? `${GREEN}enabled${RESET}`
|
|
197
|
+
: status.status === 'disabled'
|
|
198
|
+
? `${YELLOW}disabled${RESET}`
|
|
199
|
+
: status.status === 'global-broken'
|
|
200
|
+
? `${RED}disabled (global config broken)${RESET}`
|
|
201
|
+
: status.status === 'not-installed'
|
|
202
|
+
? `${RED}not installed${RESET}`
|
|
203
|
+
: `${DIM}unknown${RESET}`;
|
|
204
|
+
|
|
205
|
+
console.log(` Status: ${statusLabel}`);
|
|
206
|
+
if (status.hooksPath) {
|
|
207
|
+
console.log(` Path: ${DIM}${status.hooksPath}${RESET}`);
|
|
208
|
+
}
|
|
209
|
+
if (status.source) {
|
|
210
|
+
console.log(` Source: ${DIM}${status.source} config${RESET}`);
|
|
211
|
+
}
|
|
212
|
+
console.log('');
|
|
213
|
+
if (status.status === 'disabled') {
|
|
214
|
+
console.log(` ${DIM}Re-enable with: scd repo hooks --enable${RESET}`);
|
|
215
|
+
} else if (status.status === 'global-broken') {
|
|
216
|
+
console.log(` ${RED}Global git config has core.hooksPath set to /dev/null.${RESET}`);
|
|
217
|
+
console.log(` ${DIM}Fix with: git config --global core.hooksPath ~/.scd/hooks${RESET}`);
|
|
218
|
+
console.log(` ${DIM}Or re-enable for this repo only: scd repo hooks --enable${RESET}`);
|
|
219
|
+
} else if (status.status === 'enabled') {
|
|
220
|
+
console.log(` ${DIM}Disable for this repo: scd repo hooks --disable --reason "<reason>"${RESET}`);
|
|
221
|
+
} else {
|
|
222
|
+
console.log(` ${DIM}Install with: scd init${RESET}`);
|
|
223
|
+
}
|
|
224
|
+
console.log('');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
// ── scd hooks (global overview) ───────────────────────────────────────────
|
|
229
|
+
program
|
|
230
|
+
.command('hooks')
|
|
231
|
+
.description('Show git hook status for all known repos')
|
|
232
|
+
.action(() => {
|
|
233
|
+
const store = require('../store');
|
|
234
|
+
const { getHookStatus } = require('../hooks-manager');
|
|
235
|
+
|
|
236
|
+
const repos = store.listRepos().filter(r => !r.removed);
|
|
237
|
+
console.log(`\n${CYAN}${BOLD}Git hook status — all repos${RESET}`);
|
|
238
|
+
console.log(`${DIM}${'─'.repeat(60)}${RESET}\n`);
|
|
239
|
+
|
|
240
|
+
if (!repos.length) {
|
|
241
|
+
console.log(` ${DIM}No repos registered. Run scd init in a project.${RESET}\n`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const nameW = 28, statusW = 14;
|
|
246
|
+
console.log(
|
|
247
|
+
` ${DIM}${'Repo'.padEnd(nameW)}${'Status'.padEnd(statusW)}Source${RESET}`
|
|
248
|
+
);
|
|
249
|
+
console.log(` ${DIM}${'─'.repeat(56)}${RESET}`);
|
|
250
|
+
|
|
251
|
+
for (const repo of repos) {
|
|
252
|
+
const repoPath = repo.localPath || repo.root;
|
|
253
|
+
if (!repoPath) continue;
|
|
254
|
+
const status = getHookStatus(repoPath);
|
|
255
|
+
const name = (repo.name || repoPath.split('/').pop() || '?').slice(0, nameW - 2).padEnd(nameW);
|
|
256
|
+
// Skip repos that are missing or not git repos — not relevant for hook management
|
|
257
|
+
if (status.status === 'missing' || status.status === 'not-a-git-repo') continue;
|
|
258
|
+
|
|
259
|
+
const statusLabel = status.status === 'enabled'
|
|
260
|
+
? (GREEN + 'enabled' + RESET).padEnd(statusW + GREEN.length + RESET.length)
|
|
261
|
+
: status.status === 'disabled'
|
|
262
|
+
? (YELLOW + 'disabled' + RESET).padEnd(statusW + YELLOW.length + RESET.length)
|
|
263
|
+
: status.status === 'global-broken'
|
|
264
|
+
? (RED + 'global broken' + RESET).padEnd(statusW + RED.length + RESET.length)
|
|
265
|
+
: (DIM + status.status + RESET).padEnd(statusW + DIM.length + RESET.length);
|
|
266
|
+
const source = status.source ? `${DIM}${status.source}${RESET}` : '';
|
|
267
|
+
console.log(` ${DIM}${name}${RESET}${statusLabel}${source}`);
|
|
268
|
+
}
|
|
269
|
+
const hasBroken = repos.some(repo => { const p = repo.localPath || repo.root; return p && getHookStatus(p).status === 'global-broken'; });
|
|
270
|
+
if (hasBroken) {
|
|
271
|
+
console.log(` ${RED}⚠ Global git config has core.hooksPath set to /dev/null.${RESET}`);
|
|
272
|
+
console.log(` ${DIM}Fix: git config --global core.hooksPath ~/.scd/hooks${RESET}\n`);
|
|
273
|
+
}
|
|
274
|
+
console.log(` ${DIM}Manage hooks per repo: scd repo hooks [--disable|--enable]${RESET}\n`);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
// ── scd repo (main command) ───────────────────────────────────────────────
|
|
279
|
+
const repoCmd = new Command('repo')
|
|
280
|
+
.description('Show and manage the current repo configuration and store')
|
|
281
|
+
.option('--path', 'Print store path (for scripting)')
|
|
282
|
+
.action(async (opts) => {
|
|
283
|
+
const store = require('../store');
|
|
284
|
+
const repoRoot = getRepoRoot();
|
|
285
|
+
|
|
286
|
+
// All flags and the default view require the repo to be known.
|
|
287
|
+
// Guard here — before storeDir() which would otherwise create the directory.
|
|
288
|
+
if (!store.isRepoKnown(repoRoot)) {
|
|
289
|
+
const repoId = store.getRepoId(repoRoot);
|
|
290
|
+
console.log('\n' + YELLOW + ' This directory is not known to scd.' + RESET);
|
|
291
|
+
console.log(DIM + ' Working dir: ' + RESET + CYAN + repoRoot + RESET);
|
|
292
|
+
console.log(DIM + ' Store ID: ' + RESET + DIM + repoId + RESET + '\n');
|
|
293
|
+
console.log(' To start scanning this repo, run:');
|
|
294
|
+
console.log(CYAN + ' scd init' + RESET + DIM + ' initialise this repo and configure git hooks' + RESET);
|
|
295
|
+
console.log(CYAN + ' scd scan' + RESET + DIM + ' scan now (auto-registers on first scan)' + RESET);
|
|
296
|
+
console.log('');
|
|
297
|
+
console.log(DIM + ' Run ' + RESET + CYAN + 'scd list' + RESET + DIM + ' to see all known repos.' + RESET + '\n');
|
|
298
|
+
process.exit(0);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const dir = store.storeDir(repoRoot);
|
|
302
|
+
const identity = store.getRepoIdentity(repoRoot);
|
|
303
|
+
|
|
304
|
+
// --path – minimal output for scripting
|
|
305
|
+
if (opts.path) {
|
|
306
|
+
console.log(dir);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// --show – full meta.json for current repo
|
|
311
|
+
if (false) { // moved to scd repo show subcommand
|
|
312
|
+
const fs = require('fs');
|
|
313
|
+
const path = require('path');
|
|
314
|
+
const store = require('../store');
|
|
315
|
+
const repoRoot = getRepoRoot();
|
|
316
|
+
const dir = store.storeDir(repoRoot);
|
|
317
|
+
const metaPath = path.join(dir, 'meta.json');
|
|
318
|
+
|
|
319
|
+
let meta;
|
|
320
|
+
try {
|
|
321
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
322
|
+
} catch {
|
|
323
|
+
const repoId = store.getRepoId(repoRoot);
|
|
324
|
+
console.log('\n' + YELLOW + ' No meta.json found — this repo has not been initialised.' + RESET);
|
|
325
|
+
console.log(DIM + ' Working directory : ' + RESET + CYAN + repoRoot + RESET);
|
|
326
|
+
console.log(DIM + ' Store ID : ' + RESET + DIM + repoId + RESET);
|
|
327
|
+
console.log(DIM + ' Store path : ' + RESET + DIM + dir + RESET);
|
|
328
|
+
console.log(DIM + '\n Run ' + RESET + CYAN + 'scd init' + RESET + DIM + ' to register this repo and install git hooks.\n' + RESET);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const { cacheAge } = require('../scan-cache');
|
|
333
|
+
|
|
334
|
+
console.log('\n' + BOLD + 'Secure Code by Design – Repo meta' + RESET);
|
|
335
|
+
console.log(DIM + '─'.repeat(52) + RESET + '\n');
|
|
336
|
+
console.log(' ' + DIM + 'Working directory'.padEnd(18) + RESET + CYAN + repoRoot + RESET);
|
|
337
|
+
console.log(DIM + '─'.repeat(52) + RESET + '\n');
|
|
338
|
+
|
|
339
|
+
const row = (label, value, color) =>
|
|
340
|
+
console.log(' ' + DIM + label.padEnd(18) + RESET + (color||'') + value + RESET);
|
|
341
|
+
|
|
342
|
+
row('Name:', meta.name || '(unknown)');
|
|
343
|
+
row('Store ID:', meta.repoId, DIM);
|
|
344
|
+
row('Type:', meta.type === 'remote' ? 'remote (git)' : 'path-based (directory scan)',
|
|
345
|
+
meta.type === 'path-based' ? YELLOW : '');
|
|
346
|
+
|
|
347
|
+
if (meta.remote) row('Remote:', meta.remote, DIM);
|
|
348
|
+
row('Local path:', meta.localPath || '(none)', CYAN);
|
|
349
|
+
|
|
350
|
+
console.log();
|
|
351
|
+
|
|
352
|
+
if (meta.lastSeen) {
|
|
353
|
+
row('Last seen:', cacheAge(meta.lastSeen) + ' ' + DIM + meta.lastSeen + RESET);
|
|
354
|
+
}
|
|
355
|
+
if (meta.lastScan) {
|
|
356
|
+
const critStr = meta.lastScanCritical > 0
|
|
357
|
+
? ' ' + RED + meta.lastScanCritical + ' CRITICAL' + RESET : '';
|
|
358
|
+
row('Last scan:', cacheAge(meta.lastScan) + ' '
|
|
359
|
+
+ DIM + (meta.lastScanFindings ?? '?') + ' findings' + RESET + critStr);
|
|
360
|
+
} else {
|
|
361
|
+
row('Last scan:', '(none yet)', DIM);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Reports
|
|
365
|
+
const reports = store.listReports(repoRoot);
|
|
366
|
+
row('Reports:', reports.length + ' saved', DIM);
|
|
367
|
+
|
|
368
|
+
// Store location
|
|
369
|
+
console.log();
|
|
370
|
+
row('Store path:', dir, CYAN);
|
|
371
|
+
|
|
372
|
+
console.log();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// moved to scd repo scans subcommand
|
|
377
|
+
if (false) {
|
|
378
|
+
const store = require('../store');
|
|
379
|
+
const repoRoot = getRepoRoot();
|
|
380
|
+
const scans = store.listScans(repoRoot);
|
|
381
|
+
|
|
382
|
+
const repoId = store.getRepoId(repoRoot);
|
|
383
|
+
const scansPath = store.scansDir(repoRoot);
|
|
384
|
+
|
|
385
|
+
if (scans.length === 0) {
|
|
386
|
+
console.log('\n' + YELLOW + ' No scans found for this repo.' + RESET);
|
|
387
|
+
console.log(DIM + ' Working directory : ' + RESET + CYAN + repoRoot + RESET);
|
|
388
|
+
console.log(DIM + ' Store ID : ' + RESET + DIM + repoId + RESET);
|
|
389
|
+
console.log(DIM + ' Scans directory : ' + RESET + DIM + scansPath + RESET);
|
|
390
|
+
console.log(DIM + '\n Run ' + RESET + CYAN + 'scd scan' + RESET + DIM + ' from your project root to create a scan.\n' + RESET);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log('\n' + BOLD + 'Saved scans' + RESET + ' ' + DIM + scansPath + RESET);
|
|
395
|
+
console.log(DIM + 'Working directory: ' + RESET + CYAN + repoRoot + RESET);
|
|
396
|
+
console.log(DIM + '─'.repeat(72) + RESET);
|
|
397
|
+
console.log(DIM + 'Scan ID (UTC)'.padEnd(22) + 'Date (local)'.padEnd(22) + 'Findings'.padEnd(10) + 'Files'.padEnd(8) + 'Deep' + RESET);
|
|
398
|
+
console.log(DIM + '─'.repeat(72) + RESET);
|
|
399
|
+
|
|
400
|
+
const { cacheAge } = require('../scan-cache');
|
|
401
|
+
for (const s of scans) {
|
|
402
|
+
const date = s.scanDate ? new Date(s.scanDate).toLocaleString('en-SE') : '—';
|
|
403
|
+
const deepStr = s.hasDeep ? GREEN + '✓' + RESET : DIM + '—' + RESET;
|
|
404
|
+
console.log(
|
|
405
|
+
CYAN + s.scanId.padEnd(22) + RESET +
|
|
406
|
+
DIM + date.padEnd(22) + RESET +
|
|
407
|
+
(String(s.findingCount)).padEnd(10) +
|
|
408
|
+
DIM + String(s.totalFiles).padEnd(8) + RESET +
|
|
409
|
+
deepStr
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
console.log(DIM + '─'.repeat(72) + RESET);
|
|
413
|
+
console.log(' ' + scans.length + ' scan' + (scans.length !== 1 ? 's' : '') + ' saved\n');
|
|
414
|
+
console.log(DIM + ' Scan IDs are in UTC. Date column shows your local time.' + RESET);
|
|
415
|
+
console.log(DIM + ' scd report --scan <id> generate report from a specific scan\n' + RESET);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// moved to scd repo reports subcommand
|
|
420
|
+
if (false) {
|
|
421
|
+
const reports = store.listReports(repoRoot);
|
|
422
|
+
if (reports.length === 0) {
|
|
423
|
+
console.log(DIM + '\n No reports found. Run scd report to generate one.' + RESET + '\n');
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
console.log('\n' + BOLD + 'Saved reports' + RESET + ' ' + DIM + dir + '/reports' + RESET + '\n');
|
|
427
|
+
for (const r of reports) {
|
|
428
|
+
const size = r.size > 1024 * 1024
|
|
429
|
+
? (r.size / 1024 / 1024).toFixed(1) + ' MB'
|
|
430
|
+
: Math.round(r.size / 1024) + ' KB';
|
|
431
|
+
const age = require('../scan-cache').cacheAge(r.mtime);
|
|
432
|
+
console.log(' ' + CYAN + r.filename.padEnd(48) + RESET +
|
|
433
|
+
DIM + size.padStart(8) + ' ' + age + RESET);
|
|
434
|
+
}
|
|
435
|
+
console.log();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// moved to scd repo open / open-reports subcommands
|
|
440
|
+
if (false) {
|
|
441
|
+
const { execSync } = require('child_process');
|
|
442
|
+
const target = opts.openReports ? store.reportsDir(repoRoot) : dir;
|
|
443
|
+
const openCmd = process.platform === 'darwin' ? 'open'
|
|
444
|
+
: process.platform === 'win32' ? 'explorer'
|
|
445
|
+
: 'xdg-open';
|
|
446
|
+
try {
|
|
447
|
+
execSync(openCmd + ' "' + target + '"');
|
|
448
|
+
console.log(DIM + ' Opened: ' + target + RESET + '\n');
|
|
449
|
+
} catch {
|
|
450
|
+
console.log(YELLOW + ' Could not open file manager. Path:' + RESET + '\n ' + target + '\n');
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Default – show store info for current repo
|
|
456
|
+
const fs = require('fs');
|
|
457
|
+
const path = require('path');
|
|
458
|
+
const meta = (() => {
|
|
459
|
+
try { return JSON.parse(fs.readFileSync(path.join(dir, 'meta.json'), 'utf8')); }
|
|
460
|
+
catch { return null; }
|
|
461
|
+
})();
|
|
462
|
+
|
|
463
|
+
console.log('\n' + BOLD + 'Secure Code by Design – Store' + RESET);
|
|
464
|
+
console.log(DIM + '─'.repeat(52) + RESET + '\n');
|
|
465
|
+
console.log(' Working dir: ' + CYAN + repoRoot + RESET);
|
|
466
|
+
console.log(' Repo: ' + BOLD + (meta?.name || path.basename(repoRoot)) + RESET);
|
|
467
|
+
if (identity.type === 'remote') {
|
|
468
|
+
console.log(' Remote: ' + DIM + identity.identifier + RESET);
|
|
469
|
+
} else {
|
|
470
|
+
console.log(' Type: ' + YELLOW + 'path-based' + RESET + ' (no git remote – ID may change if folder moves)');
|
|
471
|
+
}
|
|
472
|
+
console.log(' Store ID: ' + DIM + store.getRepoId(repoRoot) + RESET);
|
|
473
|
+
console.log(' Location: ' + CYAN + dir + RESET + '\n');
|
|
474
|
+
|
|
475
|
+
if (meta?.lastScan) {
|
|
476
|
+
const { cacheAge } = require('../scan-cache');
|
|
477
|
+
const critStr = meta.lastScanCritical > 0
|
|
478
|
+
? ' ' + RED + '(' + meta.lastScanCritical + ' CRITICAL)' + RESET : '';
|
|
479
|
+
console.log(' Last scan: ' + cacheAge(meta.lastScan) +
|
|
480
|
+
' ' + DIM + meta.lastScanFindings + ' findings' + RESET + critStr);
|
|
481
|
+
} else {
|
|
482
|
+
console.log(' Last scan: ' + DIM + '(none yet)' + RESET);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const reports = store.listReports(repoRoot);
|
|
486
|
+
console.log(' Reports: ' + DIM + reports.length + ' saved' + RESET);
|
|
487
|
+
console.log();
|
|
488
|
+
console.log(' ' + DIM + 'scd repo show show full meta info for current repo' + RESET);
|
|
489
|
+
console.log(' ' + DIM + 'scd repo scans list all saved scans' + RESET);
|
|
490
|
+
console.log(' ' + DIM + 'scd repo reports list saved reports' + RESET);
|
|
491
|
+
console.log(' ' + DIM + 'scd repo open open store folder in file manager' + RESET);
|
|
492
|
+
console.log(' ' + DIM + 'scd repo open-reports open reports folder in file manager' + RESET);
|
|
493
|
+
console.log(' ' + DIM + 'scd repo scope --show show active scope exclusions' + RESET);
|
|
494
|
+
console.log(' ' + DIM + 'scd repo configure show per-repo configuration' + RESET);
|
|
495
|
+
console.log(' ' + DIM + 'scd repo configure --scan-mode fast set scan mode' + RESET);
|
|
496
|
+
console.log(' ' + DIM + 'scd repo --path print path (for scripting)' + RESET);
|
|
497
|
+
console.log(' ' + DIM + 'scd list verify verify all repos exist on disk' + RESET);
|
|
498
|
+
console.log(' ' + DIM + 'scd list verify --clean interactive cleanup of stale repos' + RESET + '\n');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
// ── scd repo scope ────────────────────────────────────────────────────────
|
|
503
|
+
const repoScopeCmd = new Command('scope')
|
|
504
|
+
.description('Manage per-repo scan scope exclusions')
|
|
505
|
+
.addHelpText('after', `
|
|
506
|
+
Examples:
|
|
507
|
+
scd repo scope --show
|
|
508
|
+
scd repo scope --add-file "tests/fixtures/" --reason "Test fixtures with intentional vulns"
|
|
509
|
+
scd repo scope --add-rule INFRA-001 --reason "Cloud-managed infrastructure"
|
|
510
|
+
scd repo scope --add-rule JS-ERR-002 --files "lib/rules/,**/*.test.js" --reason "Rule definition files"
|
|
511
|
+
|
|
512
|
+
For global (all repos) scope: scd scope --show`)
|
|
513
|
+
.option('--show', 'Show active scope exclusions for this repo (merged: global + repo + server)')
|
|
514
|
+
.option('--add-file <pattern>','Add a file/directory exclusion pattern')
|
|
515
|
+
.option('--add-rule <ruleId>', 'Add a rule exclusion')
|
|
516
|
+
.option('--files <globs>', 'Comma-separated file globs to scope a rule exclusion (use with --add-rule)')
|
|
517
|
+
.option('--reason <text>', 'Reason for the exclusion (required with --add-file and --add-rule)')
|
|
518
|
+
.option('--remove-file <pattern>','Remove a file exclusion by pattern')
|
|
519
|
+
.option('--remove-rule <ruleId>', 'Remove a rule exclusion by rule ID')
|
|
520
|
+
.action((opts) => {
|
|
521
|
+
const fs = require('fs');
|
|
522
|
+
const store = require('../store');
|
|
523
|
+
const { loadScope, validateScope, summariseScope } = require('../scope');
|
|
524
|
+
const { appendToScope, buildFileEntry, buildRuleEntry, removeFromScope } = require('../commands/scope');
|
|
525
|
+
|
|
526
|
+
const repoRoot = getRepoRoot();
|
|
527
|
+
const scopeFile = store.scopePath(repoRoot);
|
|
528
|
+
|
|
529
|
+
// ── --show ─────────────────────────────────────────────────────────────
|
|
530
|
+
if (opts.show || (!opts.addFile && !opts.addRule && !opts.removeFile && !opts.removeRule)) {
|
|
531
|
+
const scope = loadScope(repoRoot);
|
|
532
|
+
const warnings = validateScope(scope);
|
|
533
|
+
const summary = summariseScope(scope);
|
|
534
|
+
|
|
535
|
+
console.log(`\n${BOLD}Scope exclusions for this repo${RESET} ${DIM}(merged: global + repo + server)${RESET}\n`);
|
|
536
|
+
|
|
537
|
+
if (!summary.hasExclusions) {
|
|
538
|
+
console.log(`${DIM} No active exclusions.${RESET}\n`);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (summary.fileLines.length > 0) {
|
|
543
|
+
console.log(`${BOLD} File exclusions:${RESET}`);
|
|
544
|
+
for (const line of summary.fileLines) console.log(` ${line.trim()}`);
|
|
545
|
+
console.log();
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (summary.ruleLines.length > 0) {
|
|
549
|
+
console.log(`${BOLD} Rule exclusions:${RESET}`);
|
|
550
|
+
for (const line of summary.ruleLines) console.log(` ${line.trim()}`);
|
|
551
|
+
console.log();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (warnings.length > 0) {
|
|
555
|
+
console.log(`${YELLOW} ⚠ Incomplete entries (missing required fields):${RESET}`);
|
|
556
|
+
for (const w of warnings) {
|
|
557
|
+
console.log(`${YELLOW} ${w.identifier}: missing ${w.missing.join(', ')}${RESET}`);
|
|
558
|
+
}
|
|
559
|
+
console.log();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const serverFile = store.serverScopePath(repoRoot);
|
|
563
|
+
if (fs.existsSync(serverFile)) {
|
|
564
|
+
console.log(`${DIM} Server scope active (scope-server.yml) — entries marked [server] above.${RESET}\n`);
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ── require --reason ────────────────────────────────────────────────────
|
|
570
|
+
if (!opts.reason) {
|
|
571
|
+
console.error(`\n${RED}✗ --reason is required.${RESET}`);
|
|
572
|
+
console.error(` Every scope exclusion must have a documented reason.\n`);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const { getMachineFingerprint } = require('../store');
|
|
577
|
+
const installId = getMachineFingerprint() || 'unknown';
|
|
578
|
+
const addedAt = new Date().toLocaleString('sv-SE', {
|
|
579
|
+
timeZone: 'Europe/Stockholm', year: 'numeric', month: '2-digit',
|
|
580
|
+
day: '2-digit', hour: '2-digit', minute: '2-digit',
|
|
581
|
+
}).replace(',', '');
|
|
582
|
+
|
|
583
|
+
// ── --add-file ──────────────────────────────────────────────────────────
|
|
584
|
+
if (opts.addFile) {
|
|
585
|
+
const entry = buildFileEntry(opts.addFile, opts.reason, installId, addedAt);
|
|
586
|
+
appendToScope(scopeFile, 'file_excludes', entry);
|
|
587
|
+
console.log(`\n${GREEN}✓ File exclusion added to repo scope.yml${RESET}`);
|
|
588
|
+
console.log(` ${DIM}Pattern : ${opts.addFile}${RESET}`);
|
|
589
|
+
console.log(` ${DIM}Reason : ${opts.reason}${RESET}`);
|
|
590
|
+
console.log(` ${DIM}Added by: ${installId}${RESET}\n`);
|
|
591
|
+
console.log(`${YELLOW} ⚠ Active file exclusions are visible in every scan output.${RESET}\n`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ── --add-rule ──────────────────────────────────────────────────────────
|
|
596
|
+
if (opts.addRule) {
|
|
597
|
+
const files = opts.files
|
|
598
|
+
? opts.files.split(',').map(s => s.trim()).filter(Boolean)
|
|
599
|
+
: null;
|
|
600
|
+
const entry = buildRuleEntry(opts.addRule, files, opts.reason, installId, addedAt);
|
|
601
|
+
appendToScope(scopeFile, 'rule_excludes', entry);
|
|
602
|
+
const scopeDesc = files ? files.join(', ') : 'all files';
|
|
603
|
+
console.log(`\n${GREEN}✓ Rule exclusion added to repo scope.yml${RESET}`);
|
|
604
|
+
console.log(` ${DIM}Rule : ${opts.addRule}${RESET}`);
|
|
605
|
+
console.log(` ${DIM}Scope : ${scopeDesc}${RESET}`);
|
|
606
|
+
console.log(` ${DIM}Reason : ${opts.reason}${RESET}`);
|
|
607
|
+
console.log(` ${DIM}Added by: ${installId}${RESET}\n`);
|
|
608
|
+
console.log(`${YELLOW} ⚠ Active rule exclusions are visible in every scan output.${RESET}\n`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ── --remove-file ────────────────────────────────────────────────────────
|
|
613
|
+
if (opts.removeFile) {
|
|
614
|
+
const removed = removeFromScope(scopeFile, 'file_excludes', 'pattern', opts.removeFile);
|
|
615
|
+
if (removed.length === 0) {
|
|
616
|
+
console.log(`\n${YELLOW} No file exclusion found matching: ${opts.removeFile}${RESET}\n`);
|
|
617
|
+
} else {
|
|
618
|
+
console.log(`\n${GREEN}✓ Removed ${removed.length} file exclusion(s) from repo scope.yml${RESET}`);
|
|
619
|
+
for (const r of removed) {
|
|
620
|
+
console.log(` ${DIM}Pattern : ${r.pattern}${RESET}`);
|
|
621
|
+
console.log(` ${DIM}Reason : ${r.reason || '(none)'}${RESET}`);
|
|
622
|
+
console.log(` ${DIM}Added by: ${r.added_by || '(unknown)'} ${r.added_at || ''}${RESET}`);
|
|
623
|
+
}
|
|
624
|
+
console.log();
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ── --remove-rule ────────────────────────────────────────────────────────
|
|
630
|
+
if (opts.removeRule) {
|
|
631
|
+
const removed = removeFromScope(scopeFile, 'rule_excludes', 'rule', opts.removeRule);
|
|
632
|
+
if (removed.length === 0) {
|
|
633
|
+
console.log(`\n${YELLOW} No rule exclusion found matching: ${opts.removeRule}${RESET}\n`);
|
|
634
|
+
} else {
|
|
635
|
+
console.log(`\n${GREEN}✓ Removed ${removed.length} rule exclusion(s) from repo scope.yml${RESET}`);
|
|
636
|
+
for (const r of removed) {
|
|
637
|
+
const scopeDesc = r.files && r.files.length ? r.files.join(', ') : 'all files';
|
|
638
|
+
console.log(` ${DIM}Rule : ${r.rule} (${scopeDesc})${RESET}`);
|
|
639
|
+
console.log(` ${DIM}Reason : ${r.reason || '(none)'}${RESET}`);
|
|
640
|
+
console.log(` ${DIM}Added by: ${r.added_by || '(unknown)'} ${r.added_at || ''}${RESET}`);
|
|
641
|
+
}
|
|
642
|
+
console.log();
|
|
643
|
+
}
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
// ── scd repo show ─────────────────────────────────────────────────────────
|
|
650
|
+
const repoShowCmd = new Command('show')
|
|
651
|
+
.description('Show full meta info for the current repo')
|
|
652
|
+
.action(() => {
|
|
653
|
+
const fs = require('fs');
|
|
654
|
+
const path = require('path');
|
|
655
|
+
const store = require('../store');
|
|
656
|
+
const repoRoot = getRepoRoot();
|
|
657
|
+
|
|
658
|
+
if (!store.isRepoKnown(repoRoot)) {
|
|
659
|
+
console.log('\n' + YELLOW + ' This directory is not known to scd.' + RESET + '\n');
|
|
660
|
+
process.exit(0);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const dir = store.storeDir(repoRoot);
|
|
664
|
+
const metaPath = path.join(dir, 'meta.json');
|
|
665
|
+
const { cacheAge } = require('../scan-cache');
|
|
666
|
+
|
|
667
|
+
let meta;
|
|
668
|
+
try {
|
|
669
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
670
|
+
} catch {
|
|
671
|
+
const repoId = store.getRepoId(repoRoot);
|
|
672
|
+
console.log('\n' + YELLOW + ' No meta.json found — this repo has not been initialised.' + RESET);
|
|
673
|
+
console.log(DIM + ' Working directory : ' + RESET + CYAN + repoRoot + RESET);
|
|
674
|
+
console.log(DIM + ' Store ID : ' + RESET + DIM + repoId + RESET);
|
|
675
|
+
console.log(DIM + ' Store path : ' + RESET + DIM + dir + RESET);
|
|
676
|
+
console.log(DIM + '\n Run ' + RESET + CYAN + 'scd init' + RESET + DIM + ' to register this repo and install git hooks.\n' + RESET);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
console.log('\n' + BOLD + 'Secure Code by Design – Repo meta' + RESET);
|
|
681
|
+
console.log(DIM + '─'.repeat(52) + RESET + '\n');
|
|
682
|
+
console.log(' ' + DIM + 'Working directory'.padEnd(18) + RESET + CYAN + repoRoot + RESET);
|
|
683
|
+
console.log(DIM + '─'.repeat(52) + RESET + '\n');
|
|
684
|
+
|
|
685
|
+
const row = (label, value, color) =>
|
|
686
|
+
console.log(' ' + DIM + label.padEnd(18) + RESET + (color||'') + value + RESET);
|
|
687
|
+
|
|
688
|
+
row('Name:', meta.name || '(unknown)');
|
|
689
|
+
row('Store ID:', meta.repoId, DIM);
|
|
690
|
+
row('Type:', meta.type === 'remote' ? 'remote (git)' : 'path-based (directory scan)',
|
|
691
|
+
meta.type === 'path-based' ? YELLOW : '');
|
|
692
|
+
if (meta.remote) row('Remote:', meta.remote, DIM);
|
|
693
|
+
row('Local path:', meta.localPath || '(none)', CYAN);
|
|
694
|
+
console.log();
|
|
695
|
+
if (meta.lastSeen) row('Last seen:', cacheAge(meta.lastSeen) + ' ' + DIM + meta.lastSeen + RESET);
|
|
696
|
+
if (meta.lastScan) {
|
|
697
|
+
const critStr = meta.lastScanCritical > 0
|
|
698
|
+
? ' ' + RED + meta.lastScanCritical + ' CRITICAL' + RESET : '';
|
|
699
|
+
row('Last scan:', cacheAge(meta.lastScan) + ' '
|
|
700
|
+
+ DIM + (meta.lastScanFindings ?? '?') + ' findings' + RESET + critStr);
|
|
701
|
+
} else {
|
|
702
|
+
row('Last scan:', '(none yet)', DIM);
|
|
703
|
+
}
|
|
704
|
+
const reports = store.listReports(repoRoot);
|
|
705
|
+
row('Reports:', reports.length + ' saved', DIM);
|
|
706
|
+
console.log();
|
|
707
|
+
row('Store path:', dir, CYAN);
|
|
708
|
+
console.log();
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// ── scd repo scans ────────────────────────────────────────────────────────
|
|
712
|
+
const repoScansCmd = new Command('scans')
|
|
713
|
+
.description('List all saved scans for the current repo')
|
|
714
|
+
.action(() => {
|
|
715
|
+
const store = require('../store');
|
|
716
|
+
const repoRoot = getRepoRoot();
|
|
717
|
+
|
|
718
|
+
if (!store.isRepoKnown(repoRoot)) {
|
|
719
|
+
console.log('\n' + YELLOW + ' This directory is not known to scd.' + RESET + '\n');
|
|
720
|
+
process.exit(0);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const scans = store.listScans(repoRoot);
|
|
724
|
+
const repoId = store.getRepoId(repoRoot);
|
|
725
|
+
const scansPath = store.scansDir(repoRoot);
|
|
726
|
+
|
|
727
|
+
if (scans.length === 0) {
|
|
728
|
+
console.log('\n' + YELLOW + ' No scans found for this repo.' + RESET);
|
|
729
|
+
console.log(DIM + ' Working directory : ' + RESET + CYAN + repoRoot + RESET);
|
|
730
|
+
console.log(DIM + ' Store ID : ' + RESET + DIM + repoId + RESET);
|
|
731
|
+
console.log(DIM + ' Scans directory : ' + RESET + DIM + scansPath + RESET);
|
|
732
|
+
console.log(DIM + '\n Run ' + RESET + CYAN + 'scd scan' + RESET + DIM + ' from your project root to create a scan.\n' + RESET);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const { cacheAge } = require('../scan-cache');
|
|
737
|
+
console.log('\n' + BOLD + 'Saved scans' + RESET + ' ' + DIM + scansPath + RESET);
|
|
738
|
+
console.log(DIM + 'Working directory: ' + RESET + CYAN + repoRoot + RESET);
|
|
739
|
+
console.log(DIM + '─'.repeat(72) + RESET);
|
|
740
|
+
console.log(DIM + 'Scan ID (UTC)'.padEnd(22) + 'Date (local)'.padEnd(22) + 'Findings'.padEnd(10) + 'Files'.padEnd(8) + 'Deep' + RESET);
|
|
741
|
+
console.log(DIM + '─'.repeat(72) + RESET);
|
|
742
|
+
|
|
743
|
+
for (const s of scans) {
|
|
744
|
+
const date = s.scanDate ? new Date(s.scanDate).toLocaleString('en-SE') : '—';
|
|
745
|
+
const deepStr = s.hasDeep ? GREEN + '✓' + RESET : DIM + '—' + RESET;
|
|
746
|
+
console.log(
|
|
747
|
+
CYAN + s.scanId.padEnd(22) + RESET +
|
|
748
|
+
DIM + date.padEnd(22) + RESET +
|
|
749
|
+
(String(s.findingCount)).padEnd(10) +
|
|
750
|
+
DIM + String(s.totalFiles).padEnd(8) + RESET +
|
|
751
|
+
deepStr
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
console.log(DIM + '─'.repeat(72) + RESET);
|
|
755
|
+
console.log(' ' + scans.length + ' scan' + (scans.length !== 1 ? 's' : '') + ' saved\n');
|
|
756
|
+
console.log(DIM + ' Scan IDs are in UTC. Date column shows your local time.' + RESET);
|
|
757
|
+
console.log(DIM + ' scd report --scan <id> generate report from a specific scan\n' + RESET);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// ── scd repo reports ──────────────────────────────────────────────────────
|
|
761
|
+
const repoReportsCmd = new Command('reports')
|
|
762
|
+
.description('List saved reports for the current repo')
|
|
763
|
+
.action(() => {
|
|
764
|
+
const store = require('../store');
|
|
765
|
+
const repoRoot = getRepoRoot();
|
|
766
|
+
|
|
767
|
+
if (!store.isRepoKnown(repoRoot)) {
|
|
768
|
+
console.log('\n' + YELLOW + ' This directory is not known to scd.' + RESET + '\n');
|
|
769
|
+
process.exit(0);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const dir = store.storeDir(repoRoot);
|
|
773
|
+
const reports = store.listReports(repoRoot);
|
|
774
|
+
|
|
775
|
+
if (reports.length === 0) {
|
|
776
|
+
console.log(DIM + '\n No reports found. Run scd report to generate one.' + RESET + '\n');
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
console.log('\n' + BOLD + 'Saved reports' + RESET + ' ' + DIM + dir + '/reports' + RESET + '\n');
|
|
780
|
+
for (const r of reports) {
|
|
781
|
+
const size = r.size > 1024 * 1024
|
|
782
|
+
? (r.size / 1024 / 1024).toFixed(1) + ' MB'
|
|
783
|
+
: Math.round(r.size / 1024) + ' KB';
|
|
784
|
+
const age = require('../scan-cache').cacheAge(r.mtime);
|
|
785
|
+
console.log(' ' + CYAN + r.filename.padEnd(48) + RESET +
|
|
786
|
+
DIM + size.padStart(8) + ' ' + age + RESET);
|
|
787
|
+
}
|
|
788
|
+
console.log();
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
// ── scd repo open / scd repo open-reports ────────────────────────────────
|
|
792
|
+
function makeOpenCmd(cmdName, description, useReports) {
|
|
793
|
+
return new Command(cmdName)
|
|
794
|
+
.description(description)
|
|
795
|
+
.action(() => {
|
|
796
|
+
const { execSync } = require('child_process');
|
|
797
|
+
const store = require('../store');
|
|
798
|
+
const repoRoot = getRepoRoot();
|
|
799
|
+
|
|
800
|
+
if (!store.isRepoKnown(repoRoot)) {
|
|
801
|
+
console.log('\n' + YELLOW + ' This directory is not known to scd.' + RESET + '\n');
|
|
802
|
+
process.exit(0);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const dir = store.storeDir(repoRoot);
|
|
806
|
+
const target = useReports ? store.reportsDir(repoRoot) : dir;
|
|
807
|
+
const openCmd = process.platform === 'darwin' ? 'open'
|
|
808
|
+
: process.platform === 'win32' ? 'explorer'
|
|
809
|
+
: 'xdg-open';
|
|
810
|
+
try {
|
|
811
|
+
execSync(openCmd + ' "' + target + '"');
|
|
812
|
+
console.log(DIM + ' Opened: ' + target + RESET + '\n');
|
|
813
|
+
} catch {
|
|
814
|
+
console.log(YELLOW + ' Could not open file manager. Path:' + RESET + '\n ' + target + '\n');
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const repoOpenCmd = makeOpenCmd('open', 'Open store folder in Finder / Explorer / file manager', false);
|
|
820
|
+
const repoOpenReportsCmd = makeOpenCmd('open-reports', 'Open reports folder in Finder / Explorer / file manager', true);
|
|
821
|
+
|
|
822
|
+
// ── scd repo findings ─────────────────────────────────────────────────────
|
|
823
|
+
// Alias for scd findings — same flags and action, scoped to current repo
|
|
824
|
+
const repoFindingsCmd = new Command('findings')
|
|
825
|
+
.description('List findings from the last scan (alias for scd findings)')
|
|
826
|
+
.argument('[findingId]', 'Show a specific finding by ID')
|
|
827
|
+
.option('--all', 'Show all findings including excepted and resolved')
|
|
828
|
+
.option('--severity <level>', 'Filter by severity: critical, high, medium, exposure')
|
|
829
|
+
.option('--rule <id>', 'Filter by rule ID (e.g. JS-ERR-002)')
|
|
830
|
+
.option('--scan <id>', 'Load a specific scan by ID instead of last scan')
|
|
831
|
+
.option('--excepted', 'Show only excepted findings')
|
|
832
|
+
.option('--verbose', 'Show problem description, attack scenario, and fix for each finding')
|
|
833
|
+
.action(async (findingId, opts) => {
|
|
834
|
+
const { findingsAction } = require('./findings');
|
|
835
|
+
await findingsAction(findingId, opts);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// ── scd repo exceptions ────────────────────────────────────────────────────
|
|
839
|
+
// Alias for scd exceptions — same flags and action
|
|
840
|
+
const repoExceptionsCmd = new Command('exceptions')
|
|
841
|
+
.description('List exceptions and ignores for the current repo (alias for scd exceptions)')
|
|
842
|
+
.option('--list <status>', 'Filter by status: pending | approved | rejected | all (default: all)')
|
|
843
|
+
.action(async (opts) => {
|
|
844
|
+
const { listExceptions } = require('../exception-manager');
|
|
845
|
+
const { warnIfOutdated } = require('../cli-helpers');
|
|
846
|
+
const repoRoot = getRepoRoot();
|
|
847
|
+
listExceptions(repoRoot, opts.list || 'all');
|
|
848
|
+
warnIfOutdated();
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
repoCmd.addCommand(repoConfigureCmd);
|
|
852
|
+
repoCmd.addCommand(repoHooksCmd);
|
|
853
|
+
repoCmd.addCommand(repoScopeCmd);
|
|
854
|
+
repoCmd.addCommand(repoShowCmd);
|
|
855
|
+
repoCmd.addCommand(repoScansCmd);
|
|
856
|
+
repoCmd.addCommand(repoReportsCmd);
|
|
857
|
+
repoCmd.addCommand(repoOpenCmd);
|
|
858
|
+
repoCmd.addCommand(repoOpenReportsCmd);
|
|
859
|
+
repoCmd.addCommand(repoFindingsCmd);
|
|
860
|
+
repoCmd.addCommand(repoExceptionsCmd);
|
|
861
|
+
program.addCommand(repoCmd);
|
|
862
|
+
}
|