@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.
Files changed (79) hide show
  1. package/LICENSE.md +35 -0
  2. package/README.md +417 -0
  3. package/bin/scd.js +140 -0
  4. package/lib/audit-report.js +93 -0
  5. package/lib/audit-sync.js +172 -0
  6. package/lib/audit.js +356 -0
  7. package/lib/cli-helpers.js +108 -0
  8. package/lib/commands/accept.js +28 -0
  9. package/lib/commands/audit.js +17 -0
  10. package/lib/commands/configure.js +200 -0
  11. package/lib/commands/doctor.js +14 -0
  12. package/lib/commands/exceptions.js +19 -0
  13. package/lib/commands/export-findings.js +46 -0
  14. package/lib/commands/findings.js +306 -0
  15. package/lib/commands/ignore.js +28 -0
  16. package/lib/commands/init.js +16 -0
  17. package/lib/commands/insights.js +24 -0
  18. package/lib/commands/install.js +15 -0
  19. package/lib/commands/list.js +109 -0
  20. package/lib/commands/remove.js +16 -0
  21. package/lib/commands/repo.js +862 -0
  22. package/lib/commands/report.js +234 -0
  23. package/lib/commands/resolve.js +25 -0
  24. package/lib/commands/rules.js +185 -0
  25. package/lib/commands/scan.js +519 -0
  26. package/lib/commands/scope.js +341 -0
  27. package/lib/commands/sync.js +40 -0
  28. package/lib/commands/uninstall.js +15 -0
  29. package/lib/commands/version.js +33 -0
  30. package/lib/comment-map.js +388 -0
  31. package/lib/config.js +325 -0
  32. package/lib/context-modifiers.js +211 -0
  33. package/lib/deep-analyzer.js +225 -0
  34. package/lib/doctor.js +236 -0
  35. package/lib/exception-manager.js +675 -0
  36. package/lib/export-findings.js +376 -0
  37. package/lib/file-context.js +380 -0
  38. package/lib/file-filter.js +204 -0
  39. package/lib/file-manifest.js +145 -0
  40. package/lib/git-utils.js +102 -0
  41. package/lib/global-config.js +239 -0
  42. package/lib/hooks-manager.js +130 -0
  43. package/lib/init-repo.js +147 -0
  44. package/lib/insights-analyzer.js +416 -0
  45. package/lib/insights-output.js +160 -0
  46. package/lib/installer.js +128 -0
  47. package/lib/output-constants.js +32 -0
  48. package/lib/output-terminal.js +407 -0
  49. package/lib/push-queue.js +322 -0
  50. package/lib/remove-repo.js +108 -0
  51. package/lib/repo-context.js +187 -0
  52. package/lib/report-html.js +1154 -0
  53. package/lib/report-index.js +157 -0
  54. package/lib/report-json.js +136 -0
  55. package/lib/report-markdown.js +250 -0
  56. package/lib/resolve-manager.js +148 -0
  57. package/lib/rule-registry.js +205 -0
  58. package/lib/scan-cache.js +171 -0
  59. package/lib/scan-context.js +312 -0
  60. package/lib/scan-schema.js +67 -0
  61. package/lib/scanner-full.js +681 -0
  62. package/lib/scanner-manual.js +348 -0
  63. package/lib/scanner-secrets.js +83 -0
  64. package/lib/scope.js +331 -0
  65. package/lib/store-verify.js +395 -0
  66. package/lib/store.js +310 -0
  67. package/lib/taint-register.js +196 -0
  68. package/lib/version-check.js +46 -0
  69. package/package.json +37 -0
  70. package/rules/rule-loader.js +324 -0
  71. package/rules/rules-aspx-cs.json +399 -0
  72. package/rules/rules-aspx.json +222 -0
  73. package/rules/rules-infra-leakage.json +434 -0
  74. package/rules/rules-js.json +664 -0
  75. package/rules/rules-php.json +521 -0
  76. package/rules/rules-python.json +466 -0
  77. package/rules/rules-secrets.json +99 -0
  78. package/rules/rules-sensitive-files.json +475 -0
  79. package/rules/rules-ts.json +76 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * insights-output.js
3
+ * Terminal-rendering of behavioural analysis from scd insights.
4
+ */
5
+
6
+ 'use strict';
7
+ const { RESET, BOLD, DIM, RED, GREEN, YELLOW, BLUE, CYAN } = require('./output-constants');
8
+
9
+
10
+ function bar(value, max, width = 20, color = CYAN) {
11
+ const filled = max > 0 ? Math.round((value / max) * width) : 0;
12
+ return color + '█'.repeat(filled) + DIM + '░'.repeat(width - filled) + RESET;
13
+ }
14
+
15
+ function pad(str, len) {
16
+ return String(str).slice(0, len).padEnd(len);
17
+ }
18
+
19
+ function renderInsights(analysis) {
20
+ if (analysis.empty) {
21
+ console.log(`\n${DIM} ℹ️ ${analysis.reason || 'No audit data found. Run scd scan to start collecting data.'}${RESET}\n`);
22
+ return;
23
+ }
24
+
25
+ const { meta, recurringRules, avoidance, timePatterns,
26
+ knowledgeGaps, fileHotspots, trend, developers, signals } = analysis;
27
+
28
+ // ── Header ──────────────────────────────────────────────────────────────
29
+ console.log(`\n${CYAN}${BOLD}╔═══════════════════════════════════════════════╗${RESET}`);
30
+ console.log(`${CYAN}${BOLD}║ Secure Code by Design – Behavior Insights ║${RESET}`);
31
+ console.log(`${CYAN}${BOLD}╚═══════════════════════════════════════════════╝${RESET}`);
32
+ console.log(` ${DIM}Period: ${meta.firstFinding} → ${meta.lastFinding} · ${meta.periodDays} days${RESET}`);
33
+ console.log(` ${DIM}Scans: ${meta.totalScans} · Findings: ${meta.totalFindings} · Developers: ${meta.uniqueDevelopers}${RESET}\n`);
34
+
35
+ // ── Signals (most important first) ──────────────────────────────────────────
36
+ if (signals && signals.length > 0) {
37
+ console.log(`${BOLD}Detected patterns${RESET}`);
38
+ console.log('─'.repeat(52));
39
+ for (const s of signals) {
40
+ console.log(`\n ${s.level} ${BOLD}${s.title}${RESET}`);
41
+ console.log(` ${DIM}${s.detail}${RESET}`);
42
+ console.log(` ${GREEN}→ ${s.action}${RESET}`);
43
+ }
44
+ console.log();
45
+ }
46
+
47
+ // ── Trend ────────────────────────────────────────────────────────────────
48
+ if (trend.signal !== 'INSUFFICIENT_DATA' && trend.weeks?.length > 2) {
49
+ console.log(`${BOLD}Findings per week (trend)${RESET}`);
50
+ console.log('─'.repeat(52));
51
+ const maxVal = Math.max(...trend.weeks.map(w => w.total), 1);
52
+ for (const w of trend.weeks.slice(-8)) { // visa max 8 veckor
53
+ const critStr = w.critical > 0 ? ` ${RED}(${w.critical} CRIT)${RESET}` : '';
54
+ console.log(` ${DIM}${w.week}${RESET} ${bar(w.total, maxVal, 24)} ${String(w.total).padStart(3)}${critStr}`);
55
+ }
56
+ const trendSymbol = trend.signal === 'IMPROVING' ? `${GREEN}↓ Improving${RESET}`
57
+ : trend.signal === 'WORSENING' ? `${RED}↑ Worsening${RESET}`
58
+ : `${DIM}→ Stable${RESET}`;
59
+ console.log(`\n Trend: ${trendSymbol}${trend.trendPct ? ` ${DIM}(${trend.trendPct > 0 ? '+' : ''}${trend.trendPct}%)${RESET}` : ''}\n`);
60
+ }
61
+
62
+ // ── Kunskapsgap ──────────────────────────────────────────────────────────
63
+ if (knowledgeGaps.gaps.length > 0) {
64
+ console.log(`${BOLD}Knowledge gaps – OWASP categories${RESET}`);
65
+ console.log('─'.repeat(52));
66
+ const maxCat = Math.max(...knowledgeGaps.gaps.map(g => g.count), 1);
67
+ for (const g of knowledgeGaps.gaps) {
68
+ const critStr = g.breakdown?.CRITICAL > 0 ? ` ${RED}${g.breakdown.CRITICAL}C${RESET}` : '';
69
+ const highStr = g.breakdown?.HIGH > 0 ? ` ${YELLOW}${g.breakdown.HIGH}H${RESET}` : '';
70
+ // Shorten category name for terminal
71
+ const shortCat = g.category.replace(' (OWASP A0\\d+)', '').replace(/\(OWASP .+?\)/, '').trim();
72
+ console.log(` ${pad(shortCat, 38)} ${bar(g.count, maxCat, 12)} ${String(g.count).padStart(3)} ${DIM}(${g.percent}%)${RESET}${critStr}${highStr}`);
73
+ }
74
+ console.log();
75
+ }
76
+
77
+ // ── Recurring rules ───────────────────────────────────────────────────
78
+ if (recurringRules.length > 0) {
79
+ console.log(`${BOLD}Recurring rules${RESET}`);
80
+ console.log('─'.repeat(52));
81
+ const maxR = Math.max(...recurringRules.map(r => r.count), 1);
82
+ for (const r of recurringRules.slice(0, 6)) {
83
+ const sevColor = r.severity === 'CRITICAL' ? RED : r.severity === 'HIGH' ? YELLOW : DIM;
84
+ const span = r.spanDays > 0 ? ` ${DIM}(${r.spanDays} days)${RESET}` : '';
85
+ console.log(` ${sevColor}${pad(r.ruleId, 16)}${RESET} ${bar(r.count, maxR, 14)} ${String(r.count).padStart(3)} findings${span}`);
86
+ }
87
+ console.log();
88
+ }
89
+
90
+ // ── File hotspots ──────────────────────────────────────────────────────────
91
+ if (fileHotspots.hotspots.length > 0) {
92
+ console.log(`${BOLD}File hotspots${RESET}`);
93
+ console.log('─'.repeat(52));
94
+ const maxH = Math.max(...fileHotspots.hotspots.map(h => h.count), 1);
95
+ for (const h of fileHotspots.hotspots) {
96
+ const critStr = h.critical > 0 ? ` ${RED}${h.critical} CRIT${RESET}` : '';
97
+ // Show only filename + one level up for readability
98
+ const parts = h.file.replace(/\\/g, '/').split('/');
99
+ const shortFile = parts.length > 2 ? '…/' + parts.slice(-2).join('/') : h.file;
100
+ console.log(` ${pad(shortFile, 36)} ${bar(h.count, maxH, 10)} ${String(h.count).padStart(3)}${critStr}`);
101
+ }
102
+ console.log();
103
+ }
104
+
105
+ // ── Time patterns ───────────────────────────────────────────────────────────
106
+ if (timePatterns.byHour) {
107
+ console.log(`${BOLD}Time patterns – findings per hour${RESET}`);
108
+ console.log('─'.repeat(52));
109
+ const maxHour = Math.max(...timePatterns.byHour, 1);
110
+ // Show 4-hour buckets for compact output
111
+ for (let h = 0; h < 24; h += 4) {
112
+ const bucket = timePatterns.byHour.slice(h, h + 4).reduce((a, b) => a + b, 0);
113
+ const isLate = h >= 20 || h < 4;
114
+ const label = `${String(h).padStart(2, '0')}–${String(h + 4).padStart(2, '0')}`;
115
+ const color = isLate && bucket > 0 ? YELLOW : DIM;
116
+ console.log(` ${color}${label}${RESET} ${bar(bucket, maxHour * 4, 20)} ${String(bucket).padStart(3)}`);
117
+ }
118
+ if (timePatterns.lateRate > 0) {
119
+ const lateColor = timePatterns.lateRate >= 20 ? YELLOW : DIM;
120
+ console.log(`\n Late-night findings: ${lateColor}${timePatterns.lateRate}%${RESET} ${DIM}(peak: ${timePatterns.peakHour}:00, ${timePatterns.peakWeekday})${RESET}`);
121
+ }
122
+ console.log();
123
+ }
124
+
125
+ // ── Per-developer ─────────────────────────────────────────────────────────
126
+ if (developers.isTeam) {
127
+ console.log(`${BOLD}Per developer${RESET}`);
128
+ console.log('─'.repeat(52));
129
+ console.log(` ${DIM}${'Name'.padEnd(20)} ${'Findings'.padEnd(10)} ${'CRITICAL%'.padEnd(11)} Top category${RESET}`);
130
+ console.log(` ${'─'.repeat(20)} ${'─'.repeat(9)} ${'─'.repeat(10)} ${'─'.repeat(18)}`);
131
+ for (const d of developers.developers) {
132
+ const critColor = d.criticalRate >= 50 ? RED : d.criticalRate >= 25 ? YELLOW : GREEN;
133
+ const shortCat = (d.topCategory || '–').replace(/\s*\(OWASP.+?\)/, '').slice(0, 28);
134
+ console.log(` ${pad(d.name, 20)} ${String(d.total).padStart(8)} ${critColor}${String(d.criticalRate).padStart(6)}%${RESET} ${DIM}${shortCat}${RESET}`);
135
+ }
136
+ console.log();
137
+ }
138
+
139
+ // ── Undvikandebeteende ────────────────────────────────────────────────────
140
+ if (avoidance.totalFindings > 0) {
141
+ console.log(`${BOLD}Exception handling${RESET}`);
142
+ console.log('─'.repeat(52));
143
+ const excColor = avoidance.signal === 'HIGH' ? RED : avoidance.signal === 'MEDIUM' ? YELLOW : GREEN;
144
+ console.log(` Excepted: ${excColor}${avoidance.excepted}${RESET} of ${avoidance.totalFindings} findings ${DIM}(${avoidance.exceptRate}%)${RESET}`);
145
+ if (avoidance.expiredExceptions > 0) {
146
+ console.log(` Expired: ${RED}${avoidance.expiredExceptions} exceptions have expired – action required${RESET}`);
147
+ }
148
+ if (avoidance.topApprovers.length > 0) {
149
+ const approvers = avoidance.topApprovers.map(a => `${a.key} (${a.count})`).join(', ');
150
+ console.log(` Approvers: ${DIM}${approvers}${RESET}`);
151
+ }
152
+ console.log();
153
+ }
154
+
155
+ // ── Footer ────────────────────────────────────────────────────────────────
156
+ console.log(`${DIM}${'─'.repeat(52)}${RESET}`);
157
+ console.log(`${DIM} Run 'scd audit' for event log · 'scd report' for full report${RESET}\n`);
158
+ }
159
+
160
+ module.exports = { renderInsights };
@@ -0,0 +1,128 @@
1
+ const { RESET, BOLD, DIM, RED, GREEN, YELLOW, CYAN } = require('./output-constants');
2
+ /**
3
+ * installer.js
4
+ * Sets up global git hooks that point to the CLI agent.
5
+ *
6
+ * After install, ALL repos on this machine are protected automatically.
7
+ * Uses git config --global core.hooksPath
8
+ */
9
+
10
+ const { execSync } = require('child_process');
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+
15
+ const HOOKS_DIR = path.join(os.homedir(), '.scd', 'hooks');
16
+ const CLI_PATH = path.resolve(__dirname, '../bin/scd.js');
17
+
18
+ const PRE_COMMIT_HOOK = `#!/bin/sh
19
+ # Secure Code by Design – pre-commit hook
20
+ # Scans for secrets BEFORE they enter git history
21
+ # Installed by: scd install
22
+
23
+ node "${CLI_PATH}" scan --hook=pre-commit
24
+ exit $?
25
+ `;
26
+
27
+ const PRE_PUSH_HOOK = `#!/bin/sh
28
+ # Secure Code by Design – pre-push hook
29
+ # Full security scan before code leaves this machine
30
+ # Installed by: scd install
31
+
32
+ node "${CLI_PATH}" scan --hook=pre-push
33
+ exit $?
34
+ `;
35
+
36
+ async function install() {
37
+ console.log('\nCYAN Secure Code by Design – InstallationRESET\n');
38
+
39
+ // 1. Create hooks directory
40
+ fs.mkdirSync(HOOKS_DIR, { recursive: true });
41
+ console.log(`${GREEN}✅ Hooks directory created:${RESET} ${HOOKS_DIR}`);
42
+
43
+ // 2. Write hook files
44
+ const preCommitPath = path.join(HOOKS_DIR, 'pre-commit');
45
+ const prePushPath = path.join(HOOKS_DIR, 'pre-push');
46
+
47
+ fs.writeFileSync(preCommitPath, PRE_COMMIT_HOOK, { mode: 0o755 });
48
+ fs.writeFileSync(prePushPath, PRE_PUSH_HOOK, { mode: 0o755 });
49
+ console.log(`${GREEN}✅ pre-commit hook installed${RESET} (secrets scanning)`);
50
+ console.log(`${GREEN}✅ pre-push hook installed${RESET} (full OWASP scan)`);
51
+
52
+ // 3. Configure git to use our hooks globally
53
+ try {
54
+ execSync(`git config --global core.hooksPath "${HOOKS_DIR}"`, { encoding: 'utf8' });
55
+ console.log(`${GREEN}✅ Git configuredRESET (core.hooksPath → ${HOOKS_DIR})`);
56
+ } catch (err) {
57
+ console.error(RED + '❌ Kunde inte konfigurera git:' + RESET, err.message);
58
+ console.log(YELLOW + ' Run manually:' + RESET + ' git config --global core.hooksPath "' + HOOKS_DIR + '"');
59
+ }
60
+
61
+ console.log('\nGREENBOLD Installation complete!' + RESET);
62
+ console.log(DIM + ' All git repos on this machine are now protected.' + RESET);
63
+ console.log('');
64
+ console.log(DIM + ' By using scd you agree to the disclaimer at:' + RESET);
65
+ console.log(DIM + ' https://github.com/activemindsolutions/scd/blob/main/DISCLAIMER.md' + RESET);
66
+ console.log(DIM + ' scd is a static analysis aid — it does not replace penetration testing.' + RESET + '\n');
67
+ }
68
+
69
+ async function uninstall() {
70
+
71
+ console.log(`\n${CYAN} Secure Code by Design – Uninstall${RESET}\n`);
72
+
73
+ let anyAction = false;
74
+
75
+ // 1. Remove hook files
76
+ const preCommitPath = path.join(HOOKS_DIR, 'pre-commit');
77
+ const prePushPath = path.join(HOOKS_DIR, 'pre-push');
78
+ let hooksRemoved = false;
79
+ for (const p of [preCommitPath, prePushPath]) {
80
+ if (fs.existsSync(p)) {
81
+ fs.unlinkSync(p);
82
+ hooksRemoved = true;
83
+ }
84
+ }
85
+ if (hooksRemoved) {
86
+ console.log(`${GREEN}✅ Hook files removed${RESET}`);
87
+ anyAction = true;
88
+ } else {
89
+ console.log(`${DIM} No hook files found — skipping${RESET}`);
90
+ }
91
+
92
+ // 2. Remove hooks directory if empty
93
+ if (fs.existsSync(HOOKS_DIR)) {
94
+ const remaining = fs.readdirSync(HOOKS_DIR);
95
+ if (remaining.length === 0) {
96
+ fs.rmdirSync(HOOKS_DIR);
97
+ console.log(`${GREEN}✅ Hooks directory removed${RESET} (${HOOKS_DIR})`);
98
+ } else {
99
+ console.log(`${DIM} Hooks directory not empty — left in place${RESET} (${HOOKS_DIR})`);
100
+ }
101
+ anyAction = true;
102
+ }
103
+
104
+ // 3. Remove global git core.hooksPath
105
+ try {
106
+ const current = execSync('git config --global core.hooksPath', { encoding: 'utf8' }).trim();
107
+ if (current) {
108
+ execSync('git config --global --unset core.hooksPath', { encoding: 'utf8' });
109
+ console.log(`${GREEN}✅ Global git core.hooksPath removed${RESET}`);
110
+ anyAction = true;
111
+ }
112
+ } catch (_) {
113
+ // Not set — nothing to do
114
+ console.log(`${DIM} No global core.hooksPath configured — skipping${RESET}`);
115
+ }
116
+
117
+ // 4. Leave ~/.scd/ store intact — user data (scans, audit log, exceptions)
118
+ console.log(`${DIM} ~/.scd/ store preserved — your scan history and exceptions are kept${RESET}`);
119
+ console.log(`${DIM} Remove manually with: rm -rf ~/.scd${RESET}`);
120
+
121
+ console.log(`\n${GREEN}${BOLD} Uninstall complete!${RESET}`);
122
+ if (anyAction) {
123
+ console.log(`${DIM} Git hooks are no longer active on this machine.${RESET}`);
124
+ console.log(`${DIM} Run ${RESET}scd install${DIM} to re-enable.${RESET}\n`);
125
+ }
126
+ }
127
+
128
+ module.exports = { install, uninstall };
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+ /**
3
+ * output-constants.js
4
+ * Terminal presentation constants — colours and symbols.
5
+ *
6
+ * Single source of truth for all ANSI styling used in scd CLI output.
7
+ * Import in command and lib files:
8
+ * const { CYAN, GREEN, DIM, RESET, OK, WARN, FAIL } = require('../output-constants');
9
+ * (from lib/commands/: ../output-constants)
10
+ * (from lib/: ./output-constants)
11
+ *
12
+ * DIM uses \x1b[90m (bright black) — more widely supported than \x1b[2m.
13
+ */
14
+
15
+ // ── Colours ────────────────────────────────────────────────────────────────
16
+ const RESET = '\x1b[0m';
17
+ const BOLD = '\x1b[1m';
18
+ const DIM = '\x1b[90m';
19
+ const RED = '\x1b[31m';
20
+ const GREEN = '\x1b[32m';
21
+ const YELLOW = '\x1b[33m';
22
+ const BLUE = '\x1b[34m';
23
+ const CYAN = '\x1b[36m';
24
+
25
+ // ── Symbols ────────────────────────────────────────────────────────────────
26
+ const OK = '✓'; // success
27
+ const FAIL = '✗'; // hard failure / error
28
+ const WARN = '⚠'; // warning
29
+ const DASH = '–'; // en dash — used for empty/none values
30
+ const SEP = '─'; // horizontal separator (repeat as needed: SEP.repeat(52))
31
+
32
+ module.exports = { RESET, BOLD, DIM, RED, GREEN, YELLOW, BLUE, CYAN, OK, FAIL, WARN, DASH, SEP };