codehere 1.0.1 → 1.0.2
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/README.md +36 -192
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -147
|
@@ -1,724 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared trust scoring engine — used by CLI, MCP server, and hook handler.
|
|
3
|
-
*
|
|
4
|
-
* Three scanning passes:
|
|
5
|
-
* Pass 1 (comments+regex stripped): Secrets & API keys — string literals visible, comments/regex stripped
|
|
6
|
-
* Pass 2 (fully stripped): Structural patterns — eval, execSync, innerHTML, etc.
|
|
7
|
-
* Stripped to avoid false positives from pattern definitions
|
|
8
|
-
* Pass 3 (instruction scan): Hook-layer patterns on fully stripped code
|
|
9
|
-
*
|
|
10
|
-
* All passes run locally, zero API calls.
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* Extract the child_process bindings actually imported in a file, so the
|
|
14
|
-
* exec() command-injection pattern can tell a real `cp.exec(cmd)` from a
|
|
15
|
-
* `regex.exec(str)` that happens to live in a file which also uses
|
|
16
|
-
* child_process (e.g. our own artifacts-extractor: spawns git AND runs a
|
|
17
|
-
* regex loop). namespaceAliases = the `cp` in `import * as cp` / `const cp =
|
|
18
|
-
* require(...)`; namedExec = whether `exec`/`execSync` was imported by name so
|
|
19
|
-
* a bare `exec(cmd)` is callable.
|
|
20
|
-
*/
|
|
21
|
-
export function childProcessBindings(src) {
|
|
22
|
-
const namespaceAliases = new Set();
|
|
23
|
-
let namedExec = false;
|
|
24
|
-
for (const m of src.matchAll(/import\s+(?:\*\s+as\s+)?(\w+)\s+from\s+['"](?:node:)?child_process['"]/g)) {
|
|
25
|
-
namespaceAliases.add(m[1]);
|
|
26
|
-
}
|
|
27
|
-
for (const m of src.matchAll(/(?:const|let|var|import)\s+(\w+)\s*=\s*require\(\s*['"](?:node:)?child_process['"]/g)) {
|
|
28
|
-
namespaceAliases.add(m[1]);
|
|
29
|
-
}
|
|
30
|
-
for (const m of src.matchAll(/(?:import|const|let|var)\s*\{([^}]*)\}\s*(?:from\s*|=\s*require\(\s*)['"](?:node:)?child_process['"]/g)) {
|
|
31
|
-
if (/\b(?:exec|execSync)\b/.test(m[1]))
|
|
32
|
-
namedExec = true;
|
|
33
|
-
}
|
|
34
|
-
return { namespaceAliases, namedExec };
|
|
35
|
-
}
|
|
36
|
-
export const CODE_PATTERNS = [
|
|
37
|
-
// Secrets (scan with strings visible — must see inside string literals)
|
|
38
|
-
{
|
|
39
|
-
regex: /(?:password|passwd|pass|pwd|secret|api_?key|auth_?token)\s*[:=]\s*['"`][^'"`\s]{4,}['"`]/gi,
|
|
40
|
-
severity: 'high',
|
|
41
|
-
category: 'Secrets',
|
|
42
|
-
description: 'Hardcoded credential',
|
|
43
|
-
cwe: 'CWE-798',
|
|
44
|
-
scanRaw: true,
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
regex: /(?:password|passwd|secret|api.?key|auth.?token)[^\n]*\|\|\s*['"`][^'"`\s]{3,}['"`]/gi,
|
|
48
|
-
severity: 'high',
|
|
49
|
-
category: 'Secrets',
|
|
50
|
-
description: 'Hardcoded fallback credential (env var || literal)',
|
|
51
|
-
cwe: 'CWE-798',
|
|
52
|
-
scanRaw: true,
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
// Provider key formats a dev actually hardcodes. Anthropic (sk-ant-) FIRST:
|
|
56
|
-
// it carries hyphens, so the generic sk-[alnum]{32,} branch never matched —
|
|
57
|
-
// and codehere is an Anthropic-SDK product, so it's the most likely leak.
|
|
58
|
-
// A dogfood sweep (cycle 9) then caught more misses: Stripe (sk_live_ has an
|
|
59
|
-
// underscore, not a hyphen), AWS access keys (only the .aws/credentials FILE
|
|
60
|
-
// read was flagged, never a hardcoded AKIA…), and GitHub fine-grained PATs
|
|
61
|
-
// (github_pat_, distinct from the classic ghp_).
|
|
62
|
-
// /g so a file with multiple DISTINCT hardcoded keys reports each one — a
|
|
63
|
-
// trust scan should surface every secret in one pass, not just the first
|
|
64
|
-
// (the engine's exec-loop breaks after match #1 unless the regex is global).
|
|
65
|
-
regex: /sk-ant-[a-zA-Z0-9_-]{20,}|sk-[a-zA-Z0-9]{32,}|(?:sk|rk)_(?:live|test)_[a-zA-Z0-9]{20,}|AKIA[0-9A-Z]{16}|github_pat_[A-Za-z0-9_]{22,}|xoxb-[a-zA-Z0-9-]+|ghp_[a-zA-Z0-9]{36,}/gi,
|
|
66
|
-
severity: 'critical',
|
|
67
|
-
category: 'Secrets',
|
|
68
|
-
description: 'Hardcoded API key / secret (Anthropic, OpenAI, Stripe, AWS, GitHub, Slack)',
|
|
69
|
-
cwe: 'CWE-798',
|
|
70
|
-
scanRaw: true,
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
// A hardcoded PRIVATE KEY block in source is among the worst possible leaks,
|
|
74
|
-
// and trust was blind to it (cycle 10 dogfood) — the existing key rule only
|
|
75
|
-
// flags READING a .pem/.ssh file, not an inline -----BEGIN … PRIVATE KEY-----.
|
|
76
|
-
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/gi,
|
|
77
|
-
severity: 'critical',
|
|
78
|
-
category: 'Secrets',
|
|
79
|
-
description: 'Hardcoded private key (PEM block)',
|
|
80
|
-
cwe: 'CWE-798',
|
|
81
|
-
scanRaw: true,
|
|
82
|
-
},
|
|
83
|
-
// SQL Injection (scan with strings visible — SQL lives inside string literals)
|
|
84
|
-
// Per-quote-type to allow inner quotes: "SELECT...WHERE name='" + user
|
|
85
|
-
{
|
|
86
|
-
regex: /"[^"]*SELECT[^"]*FROM[^"]*WHERE[^"]*"\s*\+/gi,
|
|
87
|
-
severity: 'critical',
|
|
88
|
-
category: 'SQL Injection',
|
|
89
|
-
description: 'SQL query built with string concatenation (double-quoted)',
|
|
90
|
-
cwe: 'CWE-89',
|
|
91
|
-
scanRaw: true,
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
regex: /'[^']*SELECT[^']*FROM[^']*WHERE[^']*'\s*\+/gi,
|
|
95
|
-
severity: 'critical',
|
|
96
|
-
category: 'SQL Injection',
|
|
97
|
-
description: 'SQL query built with string concatenation (single-quoted)',
|
|
98
|
-
cwe: 'CWE-89',
|
|
99
|
-
scanRaw: true,
|
|
100
|
-
},
|
|
101
|
-
// Require a SQL keyword inside the interpolated string — otherwise this
|
|
102
|
-
// matches any `query("prompt with ${x}")` (handleQuery, graphql.query, etc.)
|
|
103
|
-
{
|
|
104
|
-
regex: /query\s*\(\s*['"`][^'"`]*\b(?:SELECT|INSERT|UPDATE|DELETE|WHERE|FROM)\b[^'"`]*\$\{|execute\s*\([`'"]\s*SELECT.*\$\{/gi,
|
|
105
|
-
severity: 'critical',
|
|
106
|
-
category: 'SQL Injection',
|
|
107
|
-
description: 'String interpolation in SQL query',
|
|
108
|
-
cwe: 'CWE-89',
|
|
109
|
-
scanRaw: true,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
regex: /pool\.query\s*\([^,)]*\+[^,)]*\)/g,
|
|
113
|
-
severity: 'high',
|
|
114
|
-
category: 'SQL Injection',
|
|
115
|
-
description: 'String concatenation in SQL query call',
|
|
116
|
-
cwe: 'CWE-89',
|
|
117
|
-
},
|
|
118
|
-
// SQL built in a template literal — catches the case the `query(...)` pattern above misses:
|
|
119
|
-
// the query is assigned to a variable first (`const q = `SELECT...${x}``) then passed to query(q).
|
|
120
|
-
// Anchored on real SQL shape (SELECT…FROM / INSERT…INTO / UPDATE…SET / DELETE…FROM) to stay low-noise.
|
|
121
|
-
{
|
|
122
|
-
regex: /`[^`]*\b(?:SELECT\b[^`]*\bFROM|INSERT\b[^`]*\bINTO|UPDATE\b[^`]*\bSET|DELETE\b[^`]*\bFROM)\b[^`]*\$\{/gi,
|
|
123
|
-
severity: 'critical',
|
|
124
|
-
category: 'SQL Injection',
|
|
125
|
-
description: 'SQL query built in a template literal with interpolation',
|
|
126
|
-
cwe: 'CWE-89',
|
|
127
|
-
scanRaw: true,
|
|
128
|
-
},
|
|
129
|
-
// Command injection with user input (scan with strings visible)
|
|
130
|
-
// Catches require('child_process').execSync(req.body.cmd) — child_process is in a string
|
|
131
|
-
// but the method call and req. reference are in plain code
|
|
132
|
-
{
|
|
133
|
-
regex: /\.exec(?:Sync)?\s*\(\s*req\./gi,
|
|
134
|
-
severity: 'critical',
|
|
135
|
-
category: 'Command Injection',
|
|
136
|
-
description: 'exec/execSync called directly with request input',
|
|
137
|
-
cwe: 'CWE-78',
|
|
138
|
-
scanRaw: true,
|
|
139
|
-
},
|
|
140
|
-
// Injection (scan fully stripped — structural, must not trigger on pattern defs or comments)
|
|
141
|
-
{
|
|
142
|
-
regex: /\beval\s*\(/g,
|
|
143
|
-
severity: 'critical',
|
|
144
|
-
category: 'Code Injection',
|
|
145
|
-
description: 'eval() allows arbitrary code execution',
|
|
146
|
-
cwe: 'CWE-95',
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
regex: /new\s+Function\s*\(/g,
|
|
150
|
-
severity: 'critical',
|
|
151
|
-
category: 'Code Injection',
|
|
152
|
-
description: 'Function() constructor execution',
|
|
153
|
-
cwe: 'CWE-95',
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
regex: /child_process\.exec(?:Sync)?\s*\(\s*[^,)'"]*(?:req\.|body\.|params\.|query\.)/g,
|
|
157
|
-
severity: 'critical',
|
|
158
|
-
category: 'Command Injection',
|
|
159
|
-
description: 'execSync/exec with user-controlled input',
|
|
160
|
-
cwe: 'CWE-78',
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
regex: /child_process\.exec\s*\(\s*[^,)]*\+/g,
|
|
164
|
-
severity: 'critical',
|
|
165
|
-
category: 'Command Injection',
|
|
166
|
-
description: 'Command injection via string concat',
|
|
167
|
-
cwe: 'CWE-78',
|
|
168
|
-
},
|
|
169
|
-
// Destructured exec/execSync with string concat containing user input
|
|
170
|
-
{
|
|
171
|
-
regex: /\bexec(?:Sync)?\s*\([^)]*\+\s*req\./g,
|
|
172
|
-
severity: 'critical',
|
|
173
|
-
category: 'Command Injection',
|
|
174
|
-
description: 'exec() with string concatenation of user input',
|
|
175
|
-
cwe: 'CWE-78',
|
|
176
|
-
scanRaw: true,
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
regex: /\.innerHTML\s*=\s*[^=]/g,
|
|
180
|
-
severity: 'high',
|
|
181
|
-
category: 'XSS',
|
|
182
|
-
description: 'innerHTML assignment (potential XSS)',
|
|
183
|
-
cwe: 'CWE-79',
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
regex: /document\.write\s*\(/g,
|
|
187
|
-
severity: 'high',
|
|
188
|
-
category: 'XSS',
|
|
189
|
-
description: 'document.write can lead to XSS',
|
|
190
|
-
cwe: 'CWE-79',
|
|
191
|
-
},
|
|
192
|
-
// Insecure deserialization
|
|
193
|
-
{
|
|
194
|
-
regex: /JSON\.parse\s*\([^)]*req\.|JSON\.parse\s*\([^)]*input/gi,
|
|
195
|
-
severity: 'medium',
|
|
196
|
-
category: 'Insecure Input',
|
|
197
|
-
description: 'JSON.parse on unvalidated user input',
|
|
198
|
-
cwe: 'CWE-502',
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
regex: /yaml\.load\s*\([^)]*\)/g,
|
|
202
|
-
severity: 'high',
|
|
203
|
-
category: 'Insecure Deserialization',
|
|
204
|
-
description: 'yaml.load without safeLoad',
|
|
205
|
-
cwe: 'CWE-502',
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
regex: /pickle\.loads\s*\(/g,
|
|
209
|
-
severity: 'critical',
|
|
210
|
-
category: 'Insecure Deserialization',
|
|
211
|
-
description: 'pickle.loads on arbitrary data',
|
|
212
|
-
cwe: 'CWE-502',
|
|
213
|
-
},
|
|
214
|
-
// Auth issues
|
|
215
|
-
{
|
|
216
|
-
regex: /jwt\.verify\s*\([^,)]+,\s*[^,)]+,\s*\{\s*algorithms\s*:\s*\[\s*['"`]none['"`]/i,
|
|
217
|
-
severity: 'critical',
|
|
218
|
-
category: 'Auth Bypass',
|
|
219
|
-
description: 'JWT algorithm:none vulnerability',
|
|
220
|
-
cwe: 'CWE-347',
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
regex: /password\s*===?\s*['"`]|password\s*==\s*req\./gi,
|
|
224
|
-
severity: 'high',
|
|
225
|
-
category: 'Auth Bypass',
|
|
226
|
-
description: 'Plaintext password comparison',
|
|
227
|
-
cwe: 'CWE-256',
|
|
228
|
-
},
|
|
229
|
-
// Weak crypto
|
|
230
|
-
{
|
|
231
|
-
regex: /Math\.random\s*\(\s*\).*(?:token|key|secret|password|nonce|salt)/gi,
|
|
232
|
-
severity: 'high',
|
|
233
|
-
category: 'Weak Crypto',
|
|
234
|
-
description: 'Math.random() for security-sensitive value',
|
|
235
|
-
cwe: 'CWE-338',
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
regex: /createCipheriv\s*\([^,]*['"`](?:des|rc4|md5)[^'"]*['"`]/gi,
|
|
239
|
-
severity: 'high',
|
|
240
|
-
category: 'Weak Crypto',
|
|
241
|
-
description: 'Weak/deprecated cipher algorithm',
|
|
242
|
-
cwe: 'CWE-327',
|
|
243
|
-
},
|
|
244
|
-
// Path traversal
|
|
245
|
-
{
|
|
246
|
-
regex: /readFile\s*\([^)]*req\.|readFileSync\s*\([^)]*user/gi,
|
|
247
|
-
severity: 'high',
|
|
248
|
-
category: 'Path Traversal',
|
|
249
|
-
description: 'File read with user-controlled path',
|
|
250
|
-
cwe: 'CWE-22',
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
regex: /__dirname\s*\+\s*['"`/]*\s*\+?\s*req\.(params|query|body)/gi,
|
|
254
|
-
severity: 'high',
|
|
255
|
-
category: 'Path Traversal',
|
|
256
|
-
description: 'Path built from user input without sanitization',
|
|
257
|
-
cwe: 'CWE-22',
|
|
258
|
-
},
|
|
259
|
-
{
|
|
260
|
-
regex: /sendFile\s*\([^)]*req\.(params|query|body)/gi,
|
|
261
|
-
severity: 'high',
|
|
262
|
-
category: 'Path Traversal',
|
|
263
|
-
description: 'sendFile with user-controlled path',
|
|
264
|
-
cwe: 'CWE-22',
|
|
265
|
-
},
|
|
266
|
-
// Open redirect
|
|
267
|
-
{
|
|
268
|
-
regex: /res\.redirect\s*\([^)]*req\.(query|body|params)/gi,
|
|
269
|
-
severity: 'medium',
|
|
270
|
-
category: 'Open Redirect',
|
|
271
|
-
description: 'Redirect to user-controlled URL',
|
|
272
|
-
cwe: 'CWE-601',
|
|
273
|
-
},
|
|
274
|
-
// Server-side XSS (template injection in response)
|
|
275
|
-
{
|
|
276
|
-
regex: /res\.send\s*\(\s*`[^`]*\$\{[^}]*req\.(query|body|params)/gi,
|
|
277
|
-
severity: 'high',
|
|
278
|
-
category: 'XSS',
|
|
279
|
-
description: 'User input interpolated directly into response HTML',
|
|
280
|
-
cwe: 'CWE-79',
|
|
281
|
-
scanRaw: true,
|
|
282
|
-
},
|
|
283
|
-
{
|
|
284
|
-
regex: /res\.send\s*\([^)]*\+\s*req\.(query|body|params)/gi,
|
|
285
|
-
severity: 'high',
|
|
286
|
-
category: 'XSS',
|
|
287
|
-
description: 'User input concatenated into response',
|
|
288
|
-
cwe: 'CWE-79',
|
|
289
|
-
scanRaw: true,
|
|
290
|
-
},
|
|
291
|
-
// Command injection via variable indirection (exec with non-literal arg).
|
|
292
|
-
// Gated: only fires if file references child_process — otherwise regex.exec(str)
|
|
293
|
-
// lights up every bundled highlight.js / parser in the ecosystem.
|
|
294
|
-
// requiresContext must match an actual import, not the literal string
|
|
295
|
-
// child_process (which this scanner file itself contains as pattern data).
|
|
296
|
-
{
|
|
297
|
-
// Capture an optional receiver (group 1) so matchFilter can check it:
|
|
298
|
-
// cp.exec(cmd) → receiver "cp" | exec(cmd) → no receiver | g.exec(s) → "g"
|
|
299
|
-
regex: /(?:\b(\w+)\s*\.\s*)?\bexec(?:Sync)?\s*\(\s*(?!['"`])(\w+)\s*[,)]/g,
|
|
300
|
-
severity: 'high',
|
|
301
|
-
category: 'Command Injection',
|
|
302
|
-
description: 'exec() called with variable argument — verify input is not user-controlled',
|
|
303
|
-
cwe: 'CWE-78',
|
|
304
|
-
requiresContext: /(?:from|require\()\s*['"](?:node:)?child_process['"]/,
|
|
305
|
-
// Only a real child_process call counts: a method call on the imported
|
|
306
|
-
// child_process binding, or a bare exec() when exec was named-imported.
|
|
307
|
-
// A method call on anything else (a RegExp, a parser, …) is not.
|
|
308
|
-
matchFilter: (m, src) => {
|
|
309
|
-
const receiver = m[1];
|
|
310
|
-
const b = childProcessBindings(src);
|
|
311
|
-
return receiver === undefined ? b.namedExec : b.namespaceAliases.has(receiver);
|
|
312
|
-
},
|
|
313
|
-
},
|
|
314
|
-
// SSRF
|
|
315
|
-
{
|
|
316
|
-
regex: /fetch\s*\(\s*req\.(body|query|params)/gi,
|
|
317
|
-
severity: 'high',
|
|
318
|
-
category: 'SSRF',
|
|
319
|
-
description: 'Server-side request to user-controlled URL',
|
|
320
|
-
cwe: 'CWE-918',
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
regex: /axios\s*\.\s*(?:get|post|put|delete)\s*\(\s*req\.(body|query|params)/gi,
|
|
324
|
-
severity: 'high',
|
|
325
|
-
category: 'SSRF',
|
|
326
|
-
description: 'HTTP request to user-controlled URL',
|
|
327
|
-
cwe: 'CWE-918',
|
|
328
|
-
},
|
|
329
|
-
// Weak crypto (MD5, SHA1 for passwords)
|
|
330
|
-
{
|
|
331
|
-
regex: /createHash\s*\(\s*['"`]md5['"`]\s*\)/gi,
|
|
332
|
-
severity: 'high',
|
|
333
|
-
category: 'Weak Crypto',
|
|
334
|
-
description: 'MD5 hash is cryptographically broken — use bcrypt/argon2 for passwords',
|
|
335
|
-
cwe: 'CWE-328',
|
|
336
|
-
scanRaw: true,
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
regex: /createHash\s*\(\s*['"`]sha1['"`]\s*\)/gi,
|
|
340
|
-
severity: 'medium',
|
|
341
|
-
category: 'Weak Crypto',
|
|
342
|
-
description: 'SHA1 is deprecated for security use — use SHA-256+ or bcrypt',
|
|
343
|
-
cwe: 'CWE-328',
|
|
344
|
-
scanRaw: true,
|
|
345
|
-
},
|
|
346
|
-
// JWT algorithm:none (both sign and verify — scan raw to catch string content)
|
|
347
|
-
{
|
|
348
|
-
regex: /jwt\.(?:sign|verify)\s*\([^)]*algorithm\s*:\s*['"`]none['"`]/gi,
|
|
349
|
-
severity: 'critical',
|
|
350
|
-
category: 'Auth Bypass',
|
|
351
|
-
description: 'JWT with algorithm "none" disables signature verification',
|
|
352
|
-
cwe: 'CWE-347',
|
|
353
|
-
scanRaw: true,
|
|
354
|
-
},
|
|
355
|
-
// Secret-file read / exfiltration. Code that touches a private key, cloud
|
|
356
|
-
// credential, or system password file is almost always exfil or theft.
|
|
357
|
-
// dogfood 2026-05-27 Phase F: reading ~/.ssh/id_rsa was a false-negative on a
|
|
358
|
-
// product that claims a trust score on every file. scanRaw — the path lives
|
|
359
|
-
// inside a string literal. Tight path set keeps false positives near zero.
|
|
360
|
-
{
|
|
361
|
-
regex: /(?:\.ssh\/id_(?:rsa|dsa|ecdsa|ed25519)|(?:^|[^\w])id_(?:rsa|ed25519)\b|\.aws\/credentials|\.config\/gcloud\/|\.kube\/config|\.docker\/config\.json|\/etc\/shadow|\.npmrc\b|\.pem['"`])/gi,
|
|
362
|
-
severity: 'critical',
|
|
363
|
-
category: 'Secret Exfiltration',
|
|
364
|
-
description: 'Reads a private key or credential file — likely secret exfiltration',
|
|
365
|
-
cwe: 'CWE-522',
|
|
366
|
-
scanRaw: true,
|
|
367
|
-
},
|
|
368
|
-
// Destructive shell command. `rm -rf` in agent-written code is worth flagging
|
|
369
|
-
// even when it looks scoped — a stray variable or trailing slash turns it into
|
|
370
|
-
// a filesystem wipe. dogfood 2026-05-27 Phase F named this a false-negative.
|
|
371
|
-
// The runtime PreToolUse hook also blocks the worst forms at execution time;
|
|
372
|
-
// this is the static signal on the file itself. scanRaw — the command is a
|
|
373
|
-
// string literal.
|
|
374
|
-
{
|
|
375
|
-
regex: /rm\s+-[a-z]*r[a-z]*f|rm\s+-[a-z]*f[a-z]*r/gi,
|
|
376
|
-
severity: 'high',
|
|
377
|
-
category: 'Destructive Operation',
|
|
378
|
-
description: 'Recursive force-delete (rm -rf) — verify the target can never be empty or a parent path',
|
|
379
|
-
cwe: 'CWE-78',
|
|
380
|
-
scanRaw: true,
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
// rm -rf targeting root, a home glob, or a variable/concatenation → wipe risk.
|
|
384
|
-
regex: /rm\s+-[a-z]*[rf][a-z]*\s+(?:\/(?:\s|$|["'`])|~|\$\{?\w|["'`][^"'`]*["'`]\s*\+|\.\.)/gi,
|
|
385
|
-
severity: 'critical',
|
|
386
|
-
category: 'Destructive Operation',
|
|
387
|
-
description: 'rm -rf targeting root, home, or a dynamic path — can wipe unintended files',
|
|
388
|
-
cwe: 'CWE-78',
|
|
389
|
-
scanRaw: true,
|
|
390
|
-
},
|
|
391
|
-
];
|
|
392
|
-
export function measureComplexity(code) {
|
|
393
|
-
const lines = code.split('\n').length;
|
|
394
|
-
const functionMatches = code.match(/\b(function\s+\w+|=>\s*\{|\basync\s+function|\bclass\s+\w+)/g);
|
|
395
|
-
const functions = functionMatches?.length ?? 0;
|
|
396
|
-
const branchKeywords = code.match(/\b(if|else\s+if|while|for|switch|case|catch|\?\s*:)/g);
|
|
397
|
-
const cyclomaticEstimate = (branchKeywords?.length ?? 0) + 1;
|
|
398
|
-
let depth = 0, maxDepth = 0;
|
|
399
|
-
for (const char of code) {
|
|
400
|
-
if (char === '{') {
|
|
401
|
-
depth++;
|
|
402
|
-
maxDepth = Math.max(maxDepth, depth);
|
|
403
|
-
}
|
|
404
|
-
if (char === '}')
|
|
405
|
-
depth = Math.max(0, depth - 1);
|
|
406
|
-
}
|
|
407
|
-
const todos = (code.match(/\/\/\s*(TODO|FIXME|HACK|XXX)/gi) ?? []).length;
|
|
408
|
-
return { lines, functions, nestingDepth: maxDepth, cyclomaticEstimate, todos };
|
|
409
|
-
}
|
|
410
|
-
// ── Scoring ───────────────────────────────────────────────────────────────────
|
|
411
|
-
const SEVERITY_PENALTY = {
|
|
412
|
-
critical: 25,
|
|
413
|
-
high: 12,
|
|
414
|
-
medium: 5,
|
|
415
|
-
low: 2,
|
|
416
|
-
};
|
|
417
|
-
/**
|
|
418
|
-
* Returns true if the character at position `pos` follows an expression-start
|
|
419
|
-
* context (i.e., `/` here opens a regex literal, not a division operator).
|
|
420
|
-
*/
|
|
421
|
-
function isRegexStart(code, pos) {
|
|
422
|
-
let j = pos - 1;
|
|
423
|
-
while (j >= 0 && (code[j] === ' ' || code[j] === '\t'))
|
|
424
|
-
j--;
|
|
425
|
-
if (j < 0)
|
|
426
|
-
return true;
|
|
427
|
-
const prev = code[j];
|
|
428
|
-
return ('=([{,;!&|?:~^%+->'.includes(prev) ||
|
|
429
|
-
/\b(return|typeof|instanceof|in|new|delete|void|throw|case)\s*$/.test(code.slice(0, pos).trimEnd()));
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Strip ONLY comments and regex literals — string literals remain visible.
|
|
433
|
-
* Used for pass 1 (raw pattern scanning): we need to see inside string literals to find
|
|
434
|
-
* hardcoded secrets and SQL injection, but comments/regex literals that contain
|
|
435
|
-
* example patterns would otherwise cause false positives.
|
|
436
|
-
*/
|
|
437
|
-
function stripCommentsAndRegex(code) {
|
|
438
|
-
const out = [];
|
|
439
|
-
let i = 0;
|
|
440
|
-
const len = code.length;
|
|
441
|
-
while (i < len) {
|
|
442
|
-
const ch = code[i];
|
|
443
|
-
const next = code[i + 1];
|
|
444
|
-
// Single-line comment: // ...
|
|
445
|
-
if (ch === '/' && next === '/') {
|
|
446
|
-
const start = i;
|
|
447
|
-
while (i < len && code[i] !== '\n')
|
|
448
|
-
i++;
|
|
449
|
-
out.push(' '.repeat(i - start));
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
// Block comment: /* ... */
|
|
453
|
-
if (ch === '/' && next === '*') {
|
|
454
|
-
const start = i;
|
|
455
|
-
i += 2;
|
|
456
|
-
while (i < len && !(code[i - 1] === '*' && code[i] === '/'))
|
|
457
|
-
i++;
|
|
458
|
-
i++;
|
|
459
|
-
out.push(' '.repeat(i - start));
|
|
460
|
-
continue;
|
|
461
|
-
}
|
|
462
|
-
// Regex literal: /.../ — strip to avoid pattern-def false positives
|
|
463
|
-
if (ch === '/' && next !== '/' && next !== '*' && isRegexStart(code, i)) {
|
|
464
|
-
const start = i;
|
|
465
|
-
i++;
|
|
466
|
-
while (i < len && code[i] !== '/' && code[i] !== '\n') {
|
|
467
|
-
if (code[i] === '\\')
|
|
468
|
-
i++;
|
|
469
|
-
if (code[i] === '[') {
|
|
470
|
-
i++;
|
|
471
|
-
while (i < len && code[i] !== ']') {
|
|
472
|
-
if (code[i] === '\\')
|
|
473
|
-
i++;
|
|
474
|
-
i++;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
i++;
|
|
478
|
-
}
|
|
479
|
-
i++;
|
|
480
|
-
while (i < len && 'gimsuy'.includes(code[i]))
|
|
481
|
-
i++;
|
|
482
|
-
out.push(' '.repeat(i - start));
|
|
483
|
-
continue;
|
|
484
|
-
}
|
|
485
|
-
// String literals — pass through (needed to detect secrets/SQL inside them)
|
|
486
|
-
out.push(ch);
|
|
487
|
-
i++;
|
|
488
|
-
}
|
|
489
|
-
return out.join('');
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Fully strip all string literals, regex literals, and comments.
|
|
493
|
-
* Used for pass 2 (structural pattern scanning) to avoid false positives
|
|
494
|
-
* from pattern definitions, example code in strings, etc.
|
|
495
|
-
*/
|
|
496
|
-
function stripLiterals(code) {
|
|
497
|
-
const out = [];
|
|
498
|
-
let i = 0;
|
|
499
|
-
const len = code.length;
|
|
500
|
-
while (i < len) {
|
|
501
|
-
const ch = code[i];
|
|
502
|
-
const next = code[i + 1];
|
|
503
|
-
if (ch === '/' && next === '/') {
|
|
504
|
-
const start = i;
|
|
505
|
-
while (i < len && code[i] !== '\n')
|
|
506
|
-
i++;
|
|
507
|
-
out.push(' '.repeat(i - start));
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
if (ch === '/' && next === '*') {
|
|
511
|
-
const start = i;
|
|
512
|
-
i += 2;
|
|
513
|
-
while (i < len && !(code[i - 1] === '*' && code[i] === '/'))
|
|
514
|
-
i++;
|
|
515
|
-
i++;
|
|
516
|
-
out.push(' '.repeat(i - start));
|
|
517
|
-
continue;
|
|
518
|
-
}
|
|
519
|
-
if (ch === '/' && next !== '/' && next !== '*' && isRegexStart(code, i)) {
|
|
520
|
-
const start = i;
|
|
521
|
-
i++;
|
|
522
|
-
while (i < len && code[i] !== '/' && code[i] !== '\n') {
|
|
523
|
-
if (code[i] === '\\')
|
|
524
|
-
i++;
|
|
525
|
-
if (code[i] === '[') {
|
|
526
|
-
i++;
|
|
527
|
-
while (i < len && code[i] !== ']') {
|
|
528
|
-
if (code[i] === '\\')
|
|
529
|
-
i++;
|
|
530
|
-
i++;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
i++;
|
|
534
|
-
}
|
|
535
|
-
i++;
|
|
536
|
-
while (i < len && 'gimsuy'.includes(code[i]))
|
|
537
|
-
i++;
|
|
538
|
-
out.push(' '.repeat(i - start));
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
if (ch === '`') {
|
|
542
|
-
const start = i;
|
|
543
|
-
i++;
|
|
544
|
-
while (i < len && code[i] !== '`') {
|
|
545
|
-
if (code[i] === '\\')
|
|
546
|
-
i++;
|
|
547
|
-
i++;
|
|
548
|
-
}
|
|
549
|
-
i++;
|
|
550
|
-
out.push(' '.repeat(i - start));
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
if (ch === '"') {
|
|
554
|
-
const start = i;
|
|
555
|
-
i++;
|
|
556
|
-
while (i < len && code[i] !== '"' && code[i] !== '\n') {
|
|
557
|
-
if (code[i] === '\\')
|
|
558
|
-
i++;
|
|
559
|
-
i++;
|
|
560
|
-
}
|
|
561
|
-
i++;
|
|
562
|
-
out.push(' '.repeat(i - start));
|
|
563
|
-
continue;
|
|
564
|
-
}
|
|
565
|
-
if (ch === "'") {
|
|
566
|
-
const start = i;
|
|
567
|
-
i++;
|
|
568
|
-
while (i < len && code[i] !== "'" && code[i] !== '\n') {
|
|
569
|
-
if (code[i] === '\\')
|
|
570
|
-
i++;
|
|
571
|
-
i++;
|
|
572
|
-
}
|
|
573
|
-
i++;
|
|
574
|
-
out.push(' '.repeat(i - start));
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
out.push(ch);
|
|
578
|
-
i++;
|
|
579
|
-
}
|
|
580
|
-
return out.join('');
|
|
581
|
-
}
|
|
582
|
-
/**
|
|
583
|
-
* Public-by-design credential patterns — not secrets, should not flag.
|
|
584
|
-
* Firebase web API keys: Google documents these as safe to expose (security is via Firebase Rules).
|
|
585
|
-
* Stripe publishable keys: meant to be used in client code (pk_test_, pk_live_).
|
|
586
|
-
* Supabase anon keys: public by design when used with row-level security.
|
|
587
|
-
*/
|
|
588
|
-
const PUBLIC_KEY_SIGNATURES = [
|
|
589
|
-
/AIzaSy[A-Za-z0-9_-]{33}/, // Firebase / Google Cloud web API key
|
|
590
|
-
/pk_(?:test|live)_[A-Za-z0-9]{20,}/, // Stripe publishable key
|
|
591
|
-
];
|
|
592
|
-
function isKnownPublicKey(matched) {
|
|
593
|
-
if (!matched)
|
|
594
|
-
return false;
|
|
595
|
-
return PUBLIC_KEY_SIGNATURES.some((re) => re.test(matched));
|
|
596
|
-
}
|
|
597
|
-
export function scoreFile(filepath, code) {
|
|
598
|
-
const allFindings = [];
|
|
599
|
-
// Pass 1: Comments+regex stripped but strings visible — catches secrets/SQL inside literals
|
|
600
|
-
const rawForSecrets = stripCommentsAndRegex(code);
|
|
601
|
-
for (const pattern of CODE_PATTERNS.filter((p) => p.scanRaw)) {
|
|
602
|
-
let match;
|
|
603
|
-
const re = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
604
|
-
while ((match = re.exec(rawForSecrets)) !== null) {
|
|
605
|
-
const matched = match[0].substring(0, 80);
|
|
606
|
-
// Skip known public-by-design keys (Firebase, Stripe publishable, etc.)
|
|
607
|
-
if (pattern.category === 'Secrets' && isKnownPublicKey(matched)) {
|
|
608
|
-
if (!re.global)
|
|
609
|
-
break;
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
const line = code.substring(0, match.index).split('\n').length;
|
|
613
|
-
allFindings.push({
|
|
614
|
-
severity: pattern.severity,
|
|
615
|
-
category: pattern.category,
|
|
616
|
-
description: pattern.description,
|
|
617
|
-
line,
|
|
618
|
-
cwe: pattern.cwe,
|
|
619
|
-
matched,
|
|
620
|
-
});
|
|
621
|
-
if (!re.global)
|
|
622
|
-
break;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
// Pass 2: Fully stripped code — structural patterns (no false positives from literals)
|
|
626
|
-
const scannable = stripLiterals(code);
|
|
627
|
-
// Context gate test: use comment-stripped source so `// require('child_process')`
|
|
628
|
-
// in a comment doesn't count as real usage. Strings still visible so actual
|
|
629
|
-
// imports like `from 'child_process'` still match.
|
|
630
|
-
const contextTarget = rawForSecrets;
|
|
631
|
-
for (const pattern of CODE_PATTERNS.filter((p) => !p.scanRaw)) {
|
|
632
|
-
// Context gate: skip pattern if the file doesn't have the required context
|
|
633
|
-
// (e.g., .exec(var) only fires if the file uses child_process).
|
|
634
|
-
if (pattern.requiresContext && !pattern.requiresContext.test(contextTarget)) {
|
|
635
|
-
continue;
|
|
636
|
-
}
|
|
637
|
-
let match;
|
|
638
|
-
const re = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
639
|
-
while ((match = re.exec(scannable)) !== null) {
|
|
640
|
-
if (pattern.matchFilter && !pattern.matchFilter(match, contextTarget)) {
|
|
641
|
-
if (!re.global)
|
|
642
|
-
break;
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
const line = code.substring(0, match.index).split('\n').length;
|
|
646
|
-
allFindings.push({
|
|
647
|
-
severity: pattern.severity,
|
|
648
|
-
category: pattern.category,
|
|
649
|
-
description: pattern.description,
|
|
650
|
-
line,
|
|
651
|
-
cwe: pattern.cwe,
|
|
652
|
-
});
|
|
653
|
-
if (!re.global)
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
// Pass 3 (removed): hook-layer instruction patterns are designed for runtime
|
|
658
|
-
// tool_input JSON, not source code. Firing them on source lit up every docstring
|
|
659
|
-
// containing phrases like "act as a flag". The hook handler still uses scanPayload
|
|
660
|
-
// directly; source scanning stays with Pass 1 + Pass 2 only.
|
|
661
|
-
const complexity = measureComplexity(code);
|
|
662
|
-
let penalty = 0;
|
|
663
|
-
for (const f of allFindings)
|
|
664
|
-
penalty += SEVERITY_PENALTY[f.severity] ?? 2;
|
|
665
|
-
if (complexity.cyclomaticEstimate > 20)
|
|
666
|
-
penalty += 5;
|
|
667
|
-
if (complexity.cyclomaticEstimate > 50)
|
|
668
|
-
penalty += 10;
|
|
669
|
-
if (complexity.nestingDepth > 6)
|
|
670
|
-
penalty += 5;
|
|
671
|
-
if (complexity.todos > 5)
|
|
672
|
-
penalty += 3;
|
|
673
|
-
const score = Math.max(0, Math.min(100, 100 - penalty));
|
|
674
|
-
const critical = allFindings.filter((f) => f.severity === 'critical').length;
|
|
675
|
-
const high = allFindings.filter((f) => f.severity === 'high').length;
|
|
676
|
-
// Honesty floor (It-0 finding): a file with a CRITICAL finding cannot read as
|
|
677
|
-
// a passing grade — eval(userInput) + a hardcoded key scoring "B 75/100" is
|
|
678
|
-
// the optimistic-average lie. A critical caps the grade at F; a high (no
|
|
679
|
-
// critical) caps it at C. The numeric score still shows the gradient below.
|
|
680
|
-
let grade = score >= 90 ? 'A' : score >= 75 ? 'B' : score >= 60 ? 'C' : score >= 40 ? 'D' : 'F';
|
|
681
|
-
if (critical > 0)
|
|
682
|
-
grade = 'F';
|
|
683
|
-
else if (high > 0 && (grade === 'A' || grade === 'B'))
|
|
684
|
-
grade = 'C';
|
|
685
|
-
const summary = critical > 0
|
|
686
|
-
? `${critical} critical issue${critical > 1 ? 's' : ''} require immediate attention`
|
|
687
|
-
: high > 0
|
|
688
|
-
? `${high} high-severity issue${high > 1 ? 's' : ''} should be addressed before merge`
|
|
689
|
-
: allFindings.length === 0
|
|
690
|
-
? 'No issues found — clean scan'
|
|
691
|
-
: `${allFindings.length} minor issue${allFindings.length > 1 ? 's' : ''} found`;
|
|
692
|
-
return { filepath, score, grade, findings: allFindings, complexity, summary };
|
|
693
|
-
}
|
|
694
|
-
export function gradeFromScore(score) {
|
|
695
|
-
return score >= 90 ? 'A' : score >= 75 ? 'B' : score >= 60 ? 'C' : score >= 40 ? 'D' : 'F';
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* S8 · the ONE project-level aggregation. `trust`, `trust --badge`, and
|
|
699
|
-
* `report` all derive the headline score + grade from here — each command
|
|
700
|
-
* used to re-implement this reduce-and-cap block (the same drift that
|
|
701
|
-
* historically hit the file-collection rules), and the badge could disagree
|
|
702
|
-
* with the report for the same target. Same inputs → same number,
|
|
703
|
-
* structurally.
|
|
704
|
-
*/
|
|
705
|
-
export function aggregateTrust(results) {
|
|
706
|
-
const count = (sev) => results.reduce((s, r) => s + r.findings.filter((f) => f.severity === sev).length, 0);
|
|
707
|
-
const criticalCount = count('critical');
|
|
708
|
-
const highCount = count('high');
|
|
709
|
-
const mediumCount = count('medium');
|
|
710
|
-
const lowCount = count('low');
|
|
711
|
-
const totalFindings = results.reduce((s, r) => s + r.findings.length, 0);
|
|
712
|
-
const overallScore = results.length > 0 ? Math.round(results.reduce((s, r) => s + r.score, 0) / results.length) : 100;
|
|
713
|
-
// Honesty floor (It-0): a scan with any CRITICAL finding can't read as a
|
|
714
|
-
// passing grade — eval()/hardcoded-key scoring "B" is the optimistic-average
|
|
715
|
-
// lie. Critical → F; high (no critical) → no better than C. Score still
|
|
716
|
-
// shows the gradient.
|
|
717
|
-
let grade = overallScore >= 90 ? 'A' : overallScore >= 75 ? 'B' : overallScore >= 60 ? 'C' : overallScore >= 40 ? 'D' : 'F';
|
|
718
|
-
if (criticalCount > 0)
|
|
719
|
-
grade = 'F';
|
|
720
|
-
else if (highCount > 0 && (grade === 'A' || grade === 'B'))
|
|
721
|
-
grade = 'C';
|
|
722
|
-
return { overallScore, grade, criticalCount, highCount, mediumCount, lowCount, totalFindings };
|
|
723
|
-
}
|
|
724
|
-
//# sourceMappingURL=trust-scorer.js.map
|