@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,388 @@
1
+ /**
2
+ * comment-map.js
3
+ * Builds a map of comment lines for a file before rules are applied.
4
+ *
5
+ * Returns a CommentMap object that classifies each line as:
6
+ * CODE — no comment content on this line
7
+ * COMMENT — entire line is a comment (line comment or inside a block comment)
8
+ * MIXED — line contains both code and an inline comment
9
+ *
10
+ * Usage:
11
+ * const map = buildCommentMap(content, 'js');
12
+ * map.isComment(lineNumber) // true if line is COMMENT
13
+ * map.isMixed(lineNumber) // true if line is MIXED
14
+ * map.lineType(lineNumber) // 'CODE' | 'COMMENT' | 'MIXED'
15
+ *
16
+ * Design notes:
17
+ * - Content is never modified — no side-effects on matchIndex or line-offsets.
18
+ * - Block comments tracked via range state machine, not regex on each line.
19
+ * - Inline comment detection is optional (disabled by default per config).
20
+ * - Unsupported extensions return an empty map (all lines → CODE).
21
+ * - Multi-line string literals (Python triple-quotes) treated as comments
22
+ * when used as docstrings (file/function level), not when assigned to a
23
+ * variable (those are data, not comments).
24
+ *
25
+ * Future: this module is designed to become a first-class scan step in the
26
+ * proposed 6-step execution model. It produces structured line metadata that
27
+ * can feed both the code scanner (skip COMMENT lines) and a dedicated comment
28
+ * scanner (analyse COMMENT lines with separate rules or AI).
29
+ */
30
+
31
+ 'use strict';
32
+
33
+ // ── Line type constants ────────────────────────────────────────────────────
34
+ const LINE_TYPE = {
35
+ CODE: 'CODE',
36
+ COMMENT: 'COMMENT',
37
+ MIXED: 'MIXED',
38
+ };
39
+
40
+ // ── Language comment syntax ────────────────────────────────────────────────
41
+ // Each entry defines:
42
+ // lineStart {string[]} Prefixes that make an entire line a comment (trimmed)
43
+ // blockOpen {string} Block comment open marker
44
+ // blockClose {string} Block comment close marker
45
+ // inlineStart {string[]} Markers that begin an inline (end-of-line) comment
46
+ //
47
+ // Notes:
48
+ // - YAML and Shell have no block comments. # is both line and inline.
49
+ // - Python triple-quote strings are handled separately via TRIPLE_QUOTE_EXTS.
50
+ // - SQL uses -- for line comments and /* */ for block comments.
51
+ // - XML/HTML comments are <!-- --> and can span multiple lines.
52
+
53
+ const SYNTAX = {
54
+ // JavaScript / TypeScript / C# / Java — C-style comments
55
+ js: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
56
+ mjs: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
57
+ cjs: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
58
+ ts: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
59
+ jsx: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
60
+ tsx: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
61
+ cs: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
62
+ go: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
63
+ java: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
64
+ kt: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
65
+ rs: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
66
+
67
+ // PHP — supports both // and # as line comments, plus /* */
68
+ php: { lineStart: ['//', '#'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//', '#'] },
69
+
70
+ // Python — # for line/inline; triple-quoted strings handled separately
71
+ py: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
72
+
73
+ // Shell / Bash / PowerShell
74
+ sh: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
75
+ bash: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
76
+ ps1: { lineStart: ['#'], blockOpen: '<#', blockClose: '#>', inlineStart: ['#'] },
77
+
78
+ // Ruby
79
+ rb: { lineStart: ['#'], blockOpen: '=begin', blockClose: '=end', inlineStart: ['#'] },
80
+
81
+ // YAML — # only, no block comments
82
+ yml: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
83
+ yaml: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: ['#'] },
84
+
85
+ // SQL
86
+ sql: { lineStart: ['--'], blockOpen: '/*', blockClose: '*/', inlineStart: ['--'] },
87
+
88
+ // INI / CFG / properties — ; and # as line comments
89
+ ini: { lineStart: [';', '#'], blockOpen: null, blockClose: null, inlineStart: [';', '#'] },
90
+ cfg: { lineStart: [';', '#'], blockOpen: null, blockClose: null, inlineStart: [';', '#'] },
91
+ conf: { lineStart: [';', '#'], blockOpen: null, blockClose: null, inlineStart: [';', '#'] },
92
+ properties: { lineStart: ['#', '!'], blockOpen: null, blockClose: null, inlineStart: [] },
93
+
94
+ // XML / HTML — <!-- --> block comments only
95
+ xml: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
96
+ html: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
97
+
98
+ // ASP.NET / Web.config — same comment syntax as XML/HTML
99
+ aspx: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
100
+ ascx: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
101
+ master: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
102
+ config: { lineStart: [], blockOpen: '<!--', blockClose: '-->', inlineStart: [] },
103
+
104
+ // Markdown — # headings are not executable, treat as comment-like.
105
+ // Findings in Markdown headings/paragraphs are almost never actionable.
106
+ md: { lineStart: ['#'], blockOpen: null, blockClose: null, inlineStart: [] },
107
+
108
+ // Backup/original files — inherit C-style // as default since most backed-up
109
+ // source files are JS/TS/C#. Findings in backup files are rarely actionable.
110
+ bak: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
111
+ old: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
112
+ orig: { lineStart: ['//'], blockOpen: '/*', blockClose: '*/', inlineStart: ['//'] },
113
+
114
+ // Batch / Windows CMD — REM and ::
115
+ bat: { lineStart: ['rem ', 'rem\t', '::'], blockOpen: null, blockClose: null, inlineStart: [] },
116
+ cmd: { lineStart: ['rem ', 'rem\t', '::'], blockOpen: null, blockClose: null, inlineStart: [] },
117
+ };
118
+
119
+ // Extensions where triple-quoted strings act as docstrings/comments
120
+ // when at the start of a file, class, or function.
121
+ // We treat any triple-quoted string that starts on a line with no preceding code
122
+ // as a COMMENT block for the purposes of FP suppression.
123
+ const TRIPLE_QUOTE_EXTS = new Set(['py']);
124
+
125
+ // ── CommentMap ────────────────────────────────────────────────────────────
126
+
127
+ class CommentMap {
128
+ /**
129
+ * @param {Map<number, string>} lineTypes Map of 1-based lineNumber → LINE_TYPE
130
+ */
131
+ constructor(lineTypes) {
132
+ this._types = lineTypes;
133
+ }
134
+
135
+ /** Returns true if the line is entirely a comment (or inside a block comment). */
136
+ isComment(lineNumber) {
137
+ return this._types.get(lineNumber) === LINE_TYPE.COMMENT;
138
+ }
139
+
140
+ /** Returns true if the line has both code and an inline comment. */
141
+ isMixed(lineNumber) {
142
+ return this._types.get(lineNumber) === LINE_TYPE.MIXED;
143
+ }
144
+
145
+ /** Returns 'CODE' | 'COMMENT' | 'MIXED'. Defaults to 'CODE' for unknown lines. */
146
+ lineType(lineNumber) {
147
+ return this._types.get(lineNumber) || LINE_TYPE.CODE;
148
+ }
149
+
150
+ /** Total number of lines classified as COMMENT. */
151
+ get commentCount() {
152
+ let n = 0;
153
+ for (const t of this._types.values()) if (t === LINE_TYPE.COMMENT) n++;
154
+ return n;
155
+ }
156
+
157
+ /** Total number of lines classified as MIXED. */
158
+ get mixedCount() {
159
+ let n = 0;
160
+ for (const t of this._types.values()) if (t === LINE_TYPE.MIXED) n++;
161
+ return n;
162
+ }
163
+ }
164
+
165
+ // ── Empty map (for unsupported extensions) ─────────────────────────────────
166
+ const EMPTY_MAP = new CommentMap(new Map());
167
+
168
+ // ── Builder ────────────────────────────────────────────────────────────────
169
+
170
+ /**
171
+ * Build a CommentMap for the given file content and extension.
172
+ *
173
+ * @param {string} content Full file content
174
+ * @param {string} ext File extension (lowercase, without dot)
175
+ * @param {object} [opts]
176
+ * @param {boolean} [opts.inline] Classify MIXED lines (inline comments). Default: false.
177
+ * @returns {CommentMap}
178
+ */
179
+ function buildCommentMap(content, ext, opts = {}) {
180
+ const syntax = SYNTAX[ext];
181
+ if (!syntax) return EMPTY_MAP;
182
+
183
+ const { inline = false } = opts;
184
+ const lines = content.split('\n');
185
+ const lineTypes = new Map();
186
+
187
+ // State for block comment tracking
188
+ let inBlock = false;
189
+
190
+ // State for Python triple-quote docstring tracking
191
+ let inTriple = false;
192
+ let tripleMarker = null; // '"""' or "'''"
193
+
194
+ for (let i = 0; i < lines.length; i++) {
195
+ const lineNum = i + 1; // 1-based
196
+ const raw = lines[i];
197
+ const trimmed = raw.trimStart();
198
+
199
+ // ── Python triple-quote docstrings ───────────────────────────────────
200
+ if (TRIPLE_QUOTE_EXTS.has(ext)) {
201
+ if (inTriple) {
202
+ // Inside a triple-quoted string — classify as COMMENT
203
+ lineTypes.set(lineNum, LINE_TYPE.COMMENT);
204
+ // Check if this line closes the triple quote
205
+ const closeIdx = raw.indexOf(tripleMarker);
206
+ if (closeIdx !== -1) {
207
+ inTriple = false;
208
+ tripleMarker = null;
209
+ // Line that closes the triple-quote is still COMMENT —
210
+ // closing marker is part of the docstring
211
+ }
212
+ continue;
213
+ }
214
+
215
+ // Check if this line opens a triple-quoted string that acts as a docstring.
216
+ // We treat it as a docstring (COMMENT) when the line starts with the marker
217
+ // (possibly with leading whitespace) with no preceding code.
218
+ const tripleOpen = trimmed.startsWith('"""') ? '"""'
219
+ : trimmed.startsWith("'''") ? "'''"
220
+ : null;
221
+
222
+ if (tripleOpen) {
223
+ // Check if the triple-quote closes on the same line (single-line docstring)
224
+ const rest = trimmed.slice(3);
225
+ const closeIdx = rest.indexOf(tripleOpen);
226
+ if (closeIdx !== -1) {
227
+ // Same-line triple-quote: classify as COMMENT
228
+ lineTypes.set(lineNum, LINE_TYPE.COMMENT);
229
+ } else {
230
+ // Multi-line triple-quote opens here
231
+ inTriple = true;
232
+ tripleMarker = tripleOpen;
233
+ lineTypes.set(lineNum, LINE_TYPE.COMMENT);
234
+ }
235
+ continue;
236
+ }
237
+ }
238
+
239
+ // ── Block comment state ──────────────────────────────────────────────
240
+ if (syntax.blockOpen && syntax.blockClose) {
241
+ if (inBlock) {
242
+ lineTypes.set(lineNum, LINE_TYPE.COMMENT);
243
+ // Check if the block closes on this line
244
+ const closeIdx = raw.indexOf(syntax.blockClose);
245
+ if (closeIdx !== -1) {
246
+ inBlock = false;
247
+ // If there is non-whitespace content after the close marker,
248
+ // this line transitions back to CODE after the close — but since
249
+ // the close marker itself is comment content, treat the whole line
250
+ // as COMMENT for simplicity. The code after */ is rare and low-risk.
251
+ }
252
+ continue;
253
+ }
254
+
255
+ // Check if a block comment opens on this line
256
+ const openIdx = raw.indexOf(syntax.blockOpen);
257
+ if (openIdx !== -1) {
258
+ // Verify no code precedes the block open on this line
259
+ const before = raw.slice(0, openIdx).trim();
260
+
261
+ // Check if the block also closes on this same line
262
+ const afterOpen = raw.slice(openIdx + syntax.blockOpen.length);
263
+ const closeOnSame = afterOpen.indexOf(syntax.blockClose);
264
+
265
+ if (before.length === 0) {
266
+ // Block open starts the line — it's a COMMENT line
267
+ if (closeOnSame !== -1) {
268
+ // Opens and closes on same line — single-line block comment
269
+ lineTypes.set(lineNum, LINE_TYPE.COMMENT);
270
+ } else {
271
+ // Block opens but doesn't close — mark as COMMENT, enter block state
272
+ inBlock = true;
273
+ lineTypes.set(lineNum, LINE_TYPE.COMMENT);
274
+ }
275
+ } else {
276
+ // Code precedes the block open — MIXED line, block may continue
277
+ if (closeOnSame === -1) {
278
+ inBlock = true;
279
+ }
280
+ if (inline) {
281
+ lineTypes.set(lineNum, LINE_TYPE.MIXED);
282
+ }
283
+ // If inline is off, this stays CODE — block tracking still applies
284
+ }
285
+ continue;
286
+ }
287
+ }
288
+
289
+ // ── Line comment check ───────────────────────────────────────────────
290
+ if (syntax.lineStart.length > 0) {
291
+ // Batch files: 'rem' is case-insensitive
292
+ const checkStr = ext === 'bat' || ext === 'cmd'
293
+ ? trimmed.toLowerCase()
294
+ : trimmed;
295
+
296
+ const isLineComment = syntax.lineStart.some(prefix => checkStr.startsWith(prefix));
297
+ if (isLineComment) {
298
+ // ── Magic comment exception ────────────────────────────────────
299
+ // JS/TS "magic comments" begin with //# or //@ and are runtime-
300
+ // significant directives, not documentation:
301
+ // //# sourceMappingURL=file.js.map (source maps — browser reads this)
302
+ // //@ sourceURL=file.js (older source map syntax)
303
+ // //# sourceURL=... (eval source naming)
304
+ // TypeScript compiler directives (// @ts-ignore etc.) are handled
305
+ // separately: they start with "// @" (space before @) and are
306
+ // intentionally classified as CODE per design decision 2026-05-15.
307
+ if ((ext === 'js' || ext === 'ts' || ext === 'mjs' || ext === 'cjs' ||
308
+ ext === 'jsx' || ext === 'tsx') &&
309
+ (trimmed.startsWith('//#') || trimmed.startsWith('//@'))) {
310
+ // Magic comment — treat as CODE, fall through
311
+ } else {
312
+ lineTypes.set(lineNum, LINE_TYPE.COMMENT);
313
+ continue;
314
+ }
315
+ }
316
+ }
317
+
318
+ // ── Inline comment check (optional) ──────────────────────────────────
319
+ if (inline && syntax.inlineStart.length > 0) {
320
+ // Detect inline comment: look for the marker outside of string literals.
321
+ // Full string-aware parsing is expensive; we use a heuristic:
322
+ // find the first inline marker occurrence and check if it's likely
323
+ // inside a string by counting unescaped quotes before it.
324
+ const inlineMarker = findInlineCommentMarker(raw, syntax.inlineStart);
325
+ if (inlineMarker !== -1) {
326
+ lineTypes.set(lineNum, LINE_TYPE.MIXED);
327
+ continue;
328
+ }
329
+ }
330
+
331
+ // ── Default: CODE ─────────────────────────────────────────────────────
332
+ // Don't set lineTypes for CODE lines — isComment() defaults to CODE.
333
+ // This keeps the Map small (only COMMENT and MIXED lines stored).
334
+ }
335
+
336
+ return new CommentMap(lineTypes);
337
+ }
338
+
339
+ // ── Inline comment heuristic ──────────────────────────────────────────────
340
+
341
+ /**
342
+ * Find the character index of an inline comment marker in a line,
343
+ * using a simple heuristic to skip markers inside string literals.
344
+ *
345
+ * Returns -1 if no inline comment marker found outside a string.
346
+ *
347
+ * Heuristic: track whether we're inside a string by counting unescaped
348
+ * quote characters. Not perfect for complex cases (nested quotes, template
349
+ * literals) but sufficient for the common cases we care about.
350
+ *
351
+ * @param {string} line Raw line content
352
+ * @param {string[]} markers List of inline comment marker strings
353
+ * @returns {number} Index of marker, or -1
354
+ */
355
+ function findInlineCommentMarker(line, markers) {
356
+ let inString = false;
357
+ let stringChar = null;
358
+
359
+ for (let i = 0; i < line.length; i++) {
360
+ const ch = line[i];
361
+
362
+ // Toggle string state on unescaped quote characters
363
+ if (!inString && (ch === '"' || ch === "'" || ch === '`')) {
364
+ inString = true;
365
+ stringChar = ch;
366
+ continue;
367
+ }
368
+ if (inString && ch === stringChar && line[i - 1] !== '\\') {
369
+ inString = false;
370
+ stringChar = null;
371
+ continue;
372
+ }
373
+
374
+ // Skip characters inside strings
375
+ if (inString) continue;
376
+
377
+ // Check each marker at current position
378
+ for (const marker of markers) {
379
+ if (line.startsWith(marker, i)) {
380
+ return i;
381
+ }
382
+ }
383
+ }
384
+
385
+ return -1;
386
+ }
387
+
388
+ module.exports = { buildCommentMap, CommentMap, LINE_TYPE };