@oculum/scanner 1.0.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/dist/formatters/cli-terminal.d.ts +27 -0
- package/dist/formatters/cli-terminal.d.ts.map +1 -0
- package/dist/formatters/cli-terminal.js +412 -0
- package/dist/formatters/cli-terminal.js.map +1 -0
- package/dist/formatters/github-comment.d.ts +41 -0
- package/dist/formatters/github-comment.d.ts.map +1 -0
- package/dist/formatters/github-comment.js +306 -0
- package/dist/formatters/github-comment.js.map +1 -0
- package/dist/formatters/grouping.d.ts +52 -0
- package/dist/formatters/grouping.d.ts.map +1 -0
- package/dist/formatters/grouping.js +152 -0
- package/dist/formatters/grouping.js.map +1 -0
- package/dist/formatters/index.d.ts +9 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +35 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/vscode-diagnostic.d.ts +103 -0
- package/dist/formatters/vscode-diagnostic.d.ts.map +1 -0
- package/dist/formatters/vscode-diagnostic.js +151 -0
- package/dist/formatters/vscode-diagnostic.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +648 -0
- package/dist/index.js.map +1 -0
- package/dist/layer1/comments.d.ts +8 -0
- package/dist/layer1/comments.d.ts.map +1 -0
- package/dist/layer1/comments.js +203 -0
- package/dist/layer1/comments.js.map +1 -0
- package/dist/layer1/config-audit.d.ts +8 -0
- package/dist/layer1/config-audit.d.ts.map +1 -0
- package/dist/layer1/config-audit.js +252 -0
- package/dist/layer1/config-audit.js.map +1 -0
- package/dist/layer1/entropy.d.ts +8 -0
- package/dist/layer1/entropy.d.ts.map +1 -0
- package/dist/layer1/entropy.js +500 -0
- package/dist/layer1/entropy.js.map +1 -0
- package/dist/layer1/file-flags.d.ts +7 -0
- package/dist/layer1/file-flags.d.ts.map +1 -0
- package/dist/layer1/file-flags.js +112 -0
- package/dist/layer1/file-flags.js.map +1 -0
- package/dist/layer1/index.d.ts +36 -0
- package/dist/layer1/index.d.ts.map +1 -0
- package/dist/layer1/index.js +132 -0
- package/dist/layer1/index.js.map +1 -0
- package/dist/layer1/patterns.d.ts +8 -0
- package/dist/layer1/patterns.d.ts.map +1 -0
- package/dist/layer1/patterns.js +482 -0
- package/dist/layer1/patterns.js.map +1 -0
- package/dist/layer1/urls.d.ts +8 -0
- package/dist/layer1/urls.d.ts.map +1 -0
- package/dist/layer1/urls.js +296 -0
- package/dist/layer1/urls.js.map +1 -0
- package/dist/layer1/weak-crypto.d.ts +7 -0
- package/dist/layer1/weak-crypto.d.ts.map +1 -0
- package/dist/layer1/weak-crypto.js +291 -0
- package/dist/layer1/weak-crypto.js.map +1 -0
- package/dist/layer2/ai-agent-tools.d.ts +19 -0
- package/dist/layer2/ai-agent-tools.d.ts.map +1 -0
- package/dist/layer2/ai-agent-tools.js +528 -0
- package/dist/layer2/ai-agent-tools.js.map +1 -0
- package/dist/layer2/ai-endpoint-protection.d.ts +36 -0
- package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -0
- package/dist/layer2/ai-endpoint-protection.js +332 -0
- package/dist/layer2/ai-endpoint-protection.js.map +1 -0
- package/dist/layer2/ai-execution-sinks.d.ts +18 -0
- package/dist/layer2/ai-execution-sinks.d.ts.map +1 -0
- package/dist/layer2/ai-execution-sinks.js +496 -0
- package/dist/layer2/ai-execution-sinks.js.map +1 -0
- package/dist/layer2/ai-fingerprinting.d.ts +7 -0
- package/dist/layer2/ai-fingerprinting.d.ts.map +1 -0
- package/dist/layer2/ai-fingerprinting.js +654 -0
- package/dist/layer2/ai-fingerprinting.js.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts +19 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.js +356 -0
- package/dist/layer2/ai-prompt-hygiene.js.map +1 -0
- package/dist/layer2/ai-rag-safety.d.ts +21 -0
- package/dist/layer2/ai-rag-safety.d.ts.map +1 -0
- package/dist/layer2/ai-rag-safety.js +459 -0
- package/dist/layer2/ai-rag-safety.js.map +1 -0
- package/dist/layer2/ai-schema-validation.d.ts +25 -0
- package/dist/layer2/ai-schema-validation.d.ts.map +1 -0
- package/dist/layer2/ai-schema-validation.js +375 -0
- package/dist/layer2/ai-schema-validation.js.map +1 -0
- package/dist/layer2/auth-antipatterns.d.ts +20 -0
- package/dist/layer2/auth-antipatterns.d.ts.map +1 -0
- package/dist/layer2/auth-antipatterns.js +333 -0
- package/dist/layer2/auth-antipatterns.js.map +1 -0
- package/dist/layer2/byok-patterns.d.ts +12 -0
- package/dist/layer2/byok-patterns.d.ts.map +1 -0
- package/dist/layer2/byok-patterns.js +299 -0
- package/dist/layer2/byok-patterns.js.map +1 -0
- package/dist/layer2/dangerous-functions.d.ts +7 -0
- package/dist/layer2/dangerous-functions.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions.js +1375 -0
- package/dist/layer2/dangerous-functions.js.map +1 -0
- package/dist/layer2/data-exposure.d.ts +16 -0
- package/dist/layer2/data-exposure.d.ts.map +1 -0
- package/dist/layer2/data-exposure.js +279 -0
- package/dist/layer2/data-exposure.js.map +1 -0
- package/dist/layer2/framework-checks.d.ts +7 -0
- package/dist/layer2/framework-checks.d.ts.map +1 -0
- package/dist/layer2/framework-checks.js +388 -0
- package/dist/layer2/framework-checks.js.map +1 -0
- package/dist/layer2/index.d.ts +58 -0
- package/dist/layer2/index.d.ts.map +1 -0
- package/dist/layer2/index.js +380 -0
- package/dist/layer2/index.js.map +1 -0
- package/dist/layer2/logic-gates.d.ts +7 -0
- package/dist/layer2/logic-gates.d.ts.map +1 -0
- package/dist/layer2/logic-gates.js +182 -0
- package/dist/layer2/logic-gates.js.map +1 -0
- package/dist/layer2/risky-imports.d.ts +7 -0
- package/dist/layer2/risky-imports.d.ts.map +1 -0
- package/dist/layer2/risky-imports.js +161 -0
- package/dist/layer2/risky-imports.js.map +1 -0
- package/dist/layer2/variables.d.ts +8 -0
- package/dist/layer2/variables.d.ts.map +1 -0
- package/dist/layer2/variables.js +152 -0
- package/dist/layer2/variables.js.map +1 -0
- package/dist/layer3/anthropic.d.ts +83 -0
- package/dist/layer3/anthropic.d.ts.map +1 -0
- package/dist/layer3/anthropic.js +1745 -0
- package/dist/layer3/anthropic.js.map +1 -0
- package/dist/layer3/index.d.ts +24 -0
- package/dist/layer3/index.d.ts.map +1 -0
- package/dist/layer3/index.js +119 -0
- package/dist/layer3/index.js.map +1 -0
- package/dist/layer3/openai.d.ts +25 -0
- package/dist/layer3/openai.d.ts.map +1 -0
- package/dist/layer3/openai.js +238 -0
- package/dist/layer3/openai.js.map +1 -0
- package/dist/layer3/package-check.d.ts +63 -0
- package/dist/layer3/package-check.d.ts.map +1 -0
- package/dist/layer3/package-check.js +508 -0
- package/dist/layer3/package-check.js.map +1 -0
- package/dist/modes/incremental.d.ts +66 -0
- package/dist/modes/incremental.d.ts.map +1 -0
- package/dist/modes/incremental.js +200 -0
- package/dist/modes/incremental.js.map +1 -0
- package/dist/tiers.d.ts +125 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +234 -0
- package/dist/tiers.js.map +1 -0
- package/dist/types.d.ts +175 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +50 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/auth-helper-detector.d.ts +56 -0
- package/dist/utils/auth-helper-detector.d.ts.map +1 -0
- package/dist/utils/auth-helper-detector.js +360 -0
- package/dist/utils/auth-helper-detector.js.map +1 -0
- package/dist/utils/context-helpers.d.ts +96 -0
- package/dist/utils/context-helpers.d.ts.map +1 -0
- package/dist/utils/context-helpers.js +493 -0
- package/dist/utils/context-helpers.js.map +1 -0
- package/dist/utils/diff-detector.d.ts +53 -0
- package/dist/utils/diff-detector.d.ts.map +1 -0
- package/dist/utils/diff-detector.js +104 -0
- package/dist/utils/diff-detector.js.map +1 -0
- package/dist/utils/diff-parser.d.ts +80 -0
- package/dist/utils/diff-parser.d.ts.map +1 -0
- package/dist/utils/diff-parser.js +202 -0
- package/dist/utils/diff-parser.js.map +1 -0
- package/dist/utils/imported-auth-detector.d.ts +37 -0
- package/dist/utils/imported-auth-detector.d.ts.map +1 -0
- package/dist/utils/imported-auth-detector.js +251 -0
- package/dist/utils/imported-auth-detector.js.map +1 -0
- package/dist/utils/middleware-detector.d.ts +55 -0
- package/dist/utils/middleware-detector.d.ts.map +1 -0
- package/dist/utils/middleware-detector.js +260 -0
- package/dist/utils/middleware-detector.js.map +1 -0
- package/dist/utils/oauth-flow-detector.d.ts +41 -0
- package/dist/utils/oauth-flow-detector.d.ts.map +1 -0
- package/dist/utils/oauth-flow-detector.js +202 -0
- package/dist/utils/oauth-flow-detector.js.map +1 -0
- package/dist/utils/path-exclusions.d.ts +55 -0
- package/dist/utils/path-exclusions.d.ts.map +1 -0
- package/dist/utils/path-exclusions.js +222 -0
- package/dist/utils/path-exclusions.js.map +1 -0
- package/dist/utils/project-context-builder.d.ts +119 -0
- package/dist/utils/project-context-builder.d.ts.map +1 -0
- package/dist/utils/project-context-builder.js +534 -0
- package/dist/utils/project-context-builder.js.map +1 -0
- package/dist/utils/registry-clients.d.ts +93 -0
- package/dist/utils/registry-clients.d.ts.map +1 -0
- package/dist/utils/registry-clients.js +273 -0
- package/dist/utils/registry-clients.js.map +1 -0
- package/dist/utils/trpc-analyzer.d.ts +78 -0
- package/dist/utils/trpc-analyzer.d.ts.map +1 -0
- package/dist/utils/trpc-analyzer.js +297 -0
- package/dist/utils/trpc-analyzer.js.map +1 -0
- package/package.json +45 -0
- package/src/__tests__/benchmark/fixtures/false-positives.ts +227 -0
- package/src/__tests__/benchmark/fixtures/index.ts +68 -0
- package/src/__tests__/benchmark/fixtures/layer1/config-audit.ts +364 -0
- package/src/__tests__/benchmark/fixtures/layer1/hardcoded-secrets.ts +173 -0
- package/src/__tests__/benchmark/fixtures/layer1/high-entropy.ts +234 -0
- package/src/__tests__/benchmark/fixtures/layer1/index.ts +31 -0
- package/src/__tests__/benchmark/fixtures/layer1/sensitive-urls.ts +90 -0
- package/src/__tests__/benchmark/fixtures/layer1/weak-crypto.ts +197 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-agent-tools.ts +170 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-endpoint-protection.ts +418 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +189 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-fingerprinting.ts +316 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +178 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +184 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-schema-validation.ts +434 -0
- package/src/__tests__/benchmark/fixtures/layer2/auth-antipatterns.ts +159 -0
- package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +112 -0
- package/src/__tests__/benchmark/fixtures/layer2/dangerous-functions.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +168 -0
- package/src/__tests__/benchmark/fixtures/layer2/framework-checks.ts +346 -0
- package/src/__tests__/benchmark/fixtures/layer2/index.ts +67 -0
- package/src/__tests__/benchmark/fixtures/layer2/injection-vulnerabilities.ts +239 -0
- package/src/__tests__/benchmark/fixtures/layer2/logic-gates.ts +246 -0
- package/src/__tests__/benchmark/fixtures/layer2/risky-imports.ts +231 -0
- package/src/__tests__/benchmark/fixtures/layer2/variables.ts +167 -0
- package/src/__tests__/benchmark/index.ts +29 -0
- package/src/__tests__/benchmark/run-benchmark.ts +144 -0
- package/src/__tests__/benchmark/run-depth-validation.ts +206 -0
- package/src/__tests__/benchmark/run-real-world-test.ts +243 -0
- package/src/__tests__/benchmark/security-benchmark-script.ts +1737 -0
- package/src/__tests__/benchmark/tier-integration-script.ts +177 -0
- package/src/__tests__/benchmark/types.ts +144 -0
- package/src/__tests__/benchmark/utils/test-runner.ts +475 -0
- package/src/__tests__/regression/known-false-positives.test.ts +467 -0
- package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +178 -0
- package/src/__tests__/snapshots/scan-depth.test.ts +258 -0
- package/src/__tests__/validation/analyze-results.ts +542 -0
- package/src/__tests__/validation/extract-for-triage.ts +146 -0
- package/src/__tests__/validation/fp-deep-analysis.ts +327 -0
- package/src/__tests__/validation/run-validation.ts +364 -0
- package/src/__tests__/validation/triage-template.md +132 -0
- package/src/formatters/cli-terminal.ts +446 -0
- package/src/formatters/github-comment.ts +382 -0
- package/src/formatters/grouping.ts +190 -0
- package/src/formatters/index.ts +47 -0
- package/src/formatters/vscode-diagnostic.ts +243 -0
- package/src/index.ts +823 -0
- package/src/layer1/comments.ts +218 -0
- package/src/layer1/config-audit.ts +289 -0
- package/src/layer1/entropy.ts +583 -0
- package/src/layer1/file-flags.ts +127 -0
- package/src/layer1/index.ts +181 -0
- package/src/layer1/patterns.ts +516 -0
- package/src/layer1/urls.ts +334 -0
- package/src/layer1/weak-crypto.ts +328 -0
- package/src/layer2/ai-agent-tools.ts +601 -0
- package/src/layer2/ai-endpoint-protection.ts +387 -0
- package/src/layer2/ai-execution-sinks.ts +580 -0
- package/src/layer2/ai-fingerprinting.ts +758 -0
- package/src/layer2/ai-prompt-hygiene.ts +411 -0
- package/src/layer2/ai-rag-safety.ts +511 -0
- package/src/layer2/ai-schema-validation.ts +421 -0
- package/src/layer2/auth-antipatterns.ts +394 -0
- package/src/layer2/byok-patterns.ts +336 -0
- package/src/layer2/dangerous-functions.ts +1563 -0
- package/src/layer2/data-exposure.ts +315 -0
- package/src/layer2/framework-checks.ts +433 -0
- package/src/layer2/index.ts +473 -0
- package/src/layer2/logic-gates.ts +206 -0
- package/src/layer2/risky-imports.ts +186 -0
- package/src/layer2/variables.ts +166 -0
- package/src/layer3/anthropic.ts +2030 -0
- package/src/layer3/index.ts +130 -0
- package/src/layer3/package-check.ts +604 -0
- package/src/modes/incremental.ts +293 -0
- package/src/tiers.ts +318 -0
- package/src/types.ts +284 -0
- package/src/utils/auth-helper-detector.ts +443 -0
- package/src/utils/context-helpers.ts +535 -0
- package/src/utils/diff-detector.ts +135 -0
- package/src/utils/diff-parser.ts +272 -0
- package/src/utils/imported-auth-detector.ts +320 -0
- package/src/utils/middleware-detector.ts +333 -0
- package/src/utils/oauth-flow-detector.ts +246 -0
- package/src/utils/path-exclusions.ts +266 -0
- package/src/utils/project-context-builder.ts +707 -0
- package/src/utils/registry-clients.ts +351 -0
- package/src/utils/trpc-analyzer.ts +382 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental Scan Mode
|
|
3
|
+
* Optimized scanning for PR workflows - only scan changed files and surface relevant findings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ScanFile, Vulnerability, ScanResult, ScanModeConfig } from '../types'
|
|
7
|
+
import { runLayer1Scan } from '../layer1'
|
|
8
|
+
import { runLayer2Scan } from '../layer2'
|
|
9
|
+
import {
|
|
10
|
+
parseDiff,
|
|
11
|
+
parseChangedFileList,
|
|
12
|
+
filterToChangedLines,
|
|
13
|
+
getChangedFilePaths,
|
|
14
|
+
type FileDiff,
|
|
15
|
+
} from '../utils/diff-parser'
|
|
16
|
+
import { detectGlobalAuthMiddleware } from '../utils/middleware-detector'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Options for incremental scanning
|
|
20
|
+
*/
|
|
21
|
+
export interface IncrementalScanOptions {
|
|
22
|
+
/** Git diff output (if available) */
|
|
23
|
+
diffContent?: string
|
|
24
|
+
/** List of changed file paths (alternative to diff) */
|
|
25
|
+
changedFiles?: string[]
|
|
26
|
+
/** Whether to only show findings on exactly changed lines (strict) or nearby (default) */
|
|
27
|
+
strictLineMatching?: boolean
|
|
28
|
+
/** Context window for "near changed line" (default: 5) */
|
|
29
|
+
contextWindow?: number
|
|
30
|
+
/** Whether to mark findings as "introduced in this PR" */
|
|
31
|
+
markAsIntroduced?: boolean
|
|
32
|
+
/** Previous findings to compare against (for suppressing pre-existing issues) */
|
|
33
|
+
previousFindings?: Vulnerability[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Result of incremental scan with additional PR-specific metadata
|
|
38
|
+
*/
|
|
39
|
+
export interface IncrementalScanResult {
|
|
40
|
+
/** All findings (filtered to changed lines) */
|
|
41
|
+
findings: Vulnerability[]
|
|
42
|
+
/** Findings that are new in this PR */
|
|
43
|
+
introduced: Vulnerability[]
|
|
44
|
+
/** Findings that existed before (if previousFindings provided) */
|
|
45
|
+
preExisting: Vulnerability[]
|
|
46
|
+
/** Number of files scanned */
|
|
47
|
+
filesScanned: number
|
|
48
|
+
/** Number of files that were changed */
|
|
49
|
+
filesChanged: number
|
|
50
|
+
/** Parsed diff information */
|
|
51
|
+
diffs: Map<string, FileDiff>
|
|
52
|
+
/** Scan duration in ms */
|
|
53
|
+
duration: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Run an incremental scan optimized for PR workflows
|
|
58
|
+
*
|
|
59
|
+
* This scans:
|
|
60
|
+
* 1. All changed files (added + modified)
|
|
61
|
+
* 2. Files that import changed files (for context)
|
|
62
|
+
* 3. Middleware files (for auth context)
|
|
63
|
+
*
|
|
64
|
+
* And only surfaces findings on/near changed lines.
|
|
65
|
+
*/
|
|
66
|
+
export async function runIncrementalScan(
|
|
67
|
+
allFiles: ScanFile[],
|
|
68
|
+
options: IncrementalScanOptions
|
|
69
|
+
): Promise<IncrementalScanResult> {
|
|
70
|
+
const startTime = Date.now()
|
|
71
|
+
|
|
72
|
+
const {
|
|
73
|
+
diffContent,
|
|
74
|
+
changedFiles,
|
|
75
|
+
strictLineMatching = false,
|
|
76
|
+
contextWindow = 5,
|
|
77
|
+
markAsIntroduced = true,
|
|
78
|
+
previousFindings = [],
|
|
79
|
+
} = options
|
|
80
|
+
|
|
81
|
+
// Parse diff or file list to get changed files
|
|
82
|
+
let diffs: Map<string, FileDiff>
|
|
83
|
+
|
|
84
|
+
if (diffContent) {
|
|
85
|
+
diffs = parseDiff(diffContent, contextWindow)
|
|
86
|
+
} else if (changedFiles && changedFiles.length > 0) {
|
|
87
|
+
diffs = parseChangedFileList(changedFiles)
|
|
88
|
+
} else {
|
|
89
|
+
// No diff info - scan everything but don't filter
|
|
90
|
+
console.log('[Incremental] No diff info provided, scanning all files')
|
|
91
|
+
diffs = new Map()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const changedPaths = getChangedFilePaths(diffs)
|
|
95
|
+
console.log(`[Incremental] Changed files: ${changedPaths.length}`)
|
|
96
|
+
|
|
97
|
+
// Build file index for import resolution
|
|
98
|
+
const fileIndex = new Map<string, ScanFile>()
|
|
99
|
+
for (const file of allFiles) {
|
|
100
|
+
fileIndex.set(file.path, file)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Determine which files to scan
|
|
104
|
+
const filesToScan: ScanFile[] = []
|
|
105
|
+
const scannedPaths = new Set<string>()
|
|
106
|
+
|
|
107
|
+
// 1. Add all changed files
|
|
108
|
+
for (const path of changedPaths) {
|
|
109
|
+
const file = fileIndex.get(path)
|
|
110
|
+
if (file && !scannedPaths.has(path)) {
|
|
111
|
+
filesToScan.push(file)
|
|
112
|
+
scannedPaths.add(path)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. Add files that import changed files (for context)
|
|
117
|
+
// This helps detect issues where changes break dependencies
|
|
118
|
+
const importers = findImporters(allFiles, changedPaths)
|
|
119
|
+
for (const path of importers) {
|
|
120
|
+
if (!scannedPaths.has(path)) {
|
|
121
|
+
const file = fileIndex.get(path)
|
|
122
|
+
if (file) {
|
|
123
|
+
filesToScan.push(file)
|
|
124
|
+
scannedPaths.add(path)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 3. Always include middleware files for auth context
|
|
130
|
+
const middlewareFile = allFiles.find(f =>
|
|
131
|
+
f.path.includes('middleware.ts') ||
|
|
132
|
+
f.path.includes('middleware.js')
|
|
133
|
+
)
|
|
134
|
+
if (middlewareFile && !scannedPaths.has(middlewareFile.path)) {
|
|
135
|
+
filesToScan.push(middlewareFile)
|
|
136
|
+
scannedPaths.add(middlewareFile.path)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`[Incremental] Scanning ${filesToScan.length} files (${changedPaths.length} changed + ${importers.size} importers)`)
|
|
140
|
+
|
|
141
|
+
// Detect auth middleware from ALL files (for context)
|
|
142
|
+
const middlewareConfig = detectGlobalAuthMiddleware(allFiles)
|
|
143
|
+
|
|
144
|
+
// Run Layer 1 + Layer 2 on selected files
|
|
145
|
+
const layer1Result = await runLayer1Scan(filesToScan)
|
|
146
|
+
const layer2Result = await runLayer2Scan(filesToScan, { middlewareConfig })
|
|
147
|
+
|
|
148
|
+
let allFindings = [...layer1Result.vulnerabilities, ...layer2Result.vulnerabilities]
|
|
149
|
+
|
|
150
|
+
// Filter to only findings on/near changed lines
|
|
151
|
+
if (diffs.size > 0) {
|
|
152
|
+
const beforeFilter = allFindings.length
|
|
153
|
+
allFindings = filterToChangedLines(allFindings, diffs, { strictMode: strictLineMatching })
|
|
154
|
+
console.log(`[Incremental] Filtered findings: ${beforeFilter} → ${allFindings.length} (on/near changed lines)`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Mark findings as introduced and separate pre-existing
|
|
158
|
+
const introduced: Vulnerability[] = []
|
|
159
|
+
const preExisting: Vulnerability[] = []
|
|
160
|
+
|
|
161
|
+
if (previousFindings.length > 0) {
|
|
162
|
+
// Create fingerprints for previous findings
|
|
163
|
+
const previousFingerprints = new Set(
|
|
164
|
+
previousFindings.map(f => `${f.filePath}:${f.lineNumber}:${f.category}`)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
for (const finding of allFindings) {
|
|
168
|
+
const fingerprint = `${finding.filePath}:${finding.lineNumber}:${finding.category}`
|
|
169
|
+
|
|
170
|
+
if (previousFingerprints.has(fingerprint)) {
|
|
171
|
+
preExisting.push(finding)
|
|
172
|
+
} else {
|
|
173
|
+
if (markAsIntroduced) {
|
|
174
|
+
finding.validationNotes = (finding.validationNotes || '') + ' [Introduced in this PR]'
|
|
175
|
+
}
|
|
176
|
+
introduced.push(finding)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// No previous findings - all are "introduced"
|
|
181
|
+
introduced.push(...allFindings)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const duration = Date.now() - startTime
|
|
185
|
+
console.log(`[Incremental] Scan completed in ${duration}ms: ${introduced.length} new, ${preExisting.length} pre-existing`)
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
findings: allFindings,
|
|
189
|
+
introduced,
|
|
190
|
+
preExisting,
|
|
191
|
+
filesScanned: filesToScan.length,
|
|
192
|
+
filesChanged: changedPaths.length,
|
|
193
|
+
diffs,
|
|
194
|
+
duration,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Find files that import any of the changed files
|
|
200
|
+
*/
|
|
201
|
+
function findImporters(allFiles: ScanFile[], changedPaths: string[]): Set<string> {
|
|
202
|
+
const importers = new Set<string>()
|
|
203
|
+
|
|
204
|
+
// Create patterns to match imports
|
|
205
|
+
const importPatterns = changedPaths.map(path => {
|
|
206
|
+
// Remove extension for import matching
|
|
207
|
+
const withoutExt = path.replace(/\.[^/.]+$/, '')
|
|
208
|
+
// Get just the filename without path for relative imports
|
|
209
|
+
const filename = withoutExt.split('/').pop() || ''
|
|
210
|
+
return { fullPath: withoutExt, filename }
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
for (const file of allFiles) {
|
|
214
|
+
// Skip if this file is already in changed paths
|
|
215
|
+
if (changedPaths.includes(file.path)) continue
|
|
216
|
+
|
|
217
|
+
// Check if this file imports any changed file
|
|
218
|
+
for (const { fullPath, filename } of importPatterns) {
|
|
219
|
+
// Match various import patterns
|
|
220
|
+
const importRegex = new RegExp(
|
|
221
|
+
`(?:import|require)\\s*(?:\\([^)]*|[^;]*from\\s*)['"]` +
|
|
222
|
+
`(?:\\.{0,2}/)?(?:${escapeRegex(fullPath)}|[^'"]*/${escapeRegex(filename)})['"]`,
|
|
223
|
+
'i'
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if (importRegex.test(file.content)) {
|
|
227
|
+
importers.add(file.path)
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return importers
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Escape special regex characters
|
|
238
|
+
*/
|
|
239
|
+
function escapeRegex(str: string): string {
|
|
240
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Create a PR-optimized scan config
|
|
245
|
+
*/
|
|
246
|
+
export function createPRScanConfig(
|
|
247
|
+
changedFiles: string[],
|
|
248
|
+
options: Partial<ScanModeConfig> = {}
|
|
249
|
+
): ScanModeConfig {
|
|
250
|
+
return {
|
|
251
|
+
mode: 'incremental',
|
|
252
|
+
changedFiles,
|
|
253
|
+
skipAIValidation: false, // Use AI for validation
|
|
254
|
+
skipLayer3: true, // Skip deep analysis for speed
|
|
255
|
+
maxAIValidationFiles: 20,
|
|
256
|
+
maxLayer3Files: 0,
|
|
257
|
+
scanDepth: 'cheap', // Fast feedback for PRs
|
|
258
|
+
...options,
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Format incremental scan result for PR comment
|
|
264
|
+
*/
|
|
265
|
+
export function formatIncrementalForPR(result: IncrementalScanResult): {
|
|
266
|
+
summary: string
|
|
267
|
+
hasNewIssues: boolean
|
|
268
|
+
blockingIssues: number
|
|
269
|
+
} {
|
|
270
|
+
const blocking = result.introduced.filter(
|
|
271
|
+
f => f.severity === 'critical' || f.severity === 'high'
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
let summary: string
|
|
275
|
+
|
|
276
|
+
if (result.introduced.length === 0) {
|
|
277
|
+
summary = `✅ No new security issues introduced in this PR`
|
|
278
|
+
} else if (blocking.length > 0) {
|
|
279
|
+
summary = `🚨 ${blocking.length} blocking issue${blocking.length === 1 ? '' : 's'} introduced`
|
|
280
|
+
} else {
|
|
281
|
+
summary = `⚠️ ${result.introduced.length} new issue${result.introduced.length === 1 ? '' : 's'} to review`
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (result.preExisting.length > 0) {
|
|
285
|
+
summary += ` (${result.preExisting.length} pre-existing)`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
summary,
|
|
290
|
+
hasNewIssues: result.introduced.length > 0,
|
|
291
|
+
blockingIssues: blocking.length,
|
|
292
|
+
}
|
|
293
|
+
}
|
package/src/tiers.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detector Tier System
|
|
3
|
+
*
|
|
4
|
+
* Provides a shared language for "how much we trust this detector" so we can:
|
|
5
|
+
* - Filter findings in runScan by tier + depth
|
|
6
|
+
* - Log tier breakdowns for tuning
|
|
7
|
+
* - Route AI validation budget toward Tier B
|
|
8
|
+
*
|
|
9
|
+
* Security reasoning:
|
|
10
|
+
* - Makes it explicit which detectors are safe to expose in cheap scans
|
|
11
|
+
* - Avoids "accidental promotion" of an experimental heuristic to production output
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { VulnerabilityCategory } from './types'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detector tiers control visibility and trust level:
|
|
18
|
+
*
|
|
19
|
+
* - core: High-precision SAST + core AI-safety detectors. Visible in all scan depths.
|
|
20
|
+
* - ai_assisted: Context-heavy heuristics that need AI validation. Shown in validated/deep.
|
|
21
|
+
* - experimental: High-noise signals used only for internal scoring/AI hints. Hidden from users.
|
|
22
|
+
*/
|
|
23
|
+
export type DetectorTier = 'core' | 'ai_assisted' | 'experimental'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Tier statistics for logging and analysis
|
|
27
|
+
*/
|
|
28
|
+
export interface TierStats {
|
|
29
|
+
core: number
|
|
30
|
+
ai_assisted: number
|
|
31
|
+
experimental: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Layer 1 Detector Tier Mappings
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Layer 1 detector names (internal identifiers matching detector function names)
|
|
40
|
+
*/
|
|
41
|
+
export type Layer1DetectorName =
|
|
42
|
+
| 'known_secrets' // patterns.ts - Known secret patterns (API keys, tokens)
|
|
43
|
+
| 'weak_crypto' // weak-crypto.ts - Weak hash/cipher usage
|
|
44
|
+
| 'sensitive_urls' // urls.ts - Webhook URLs, URLs with tokens, internal endpoints
|
|
45
|
+
| 'entropy' // entropy.ts - High-entropy string detection
|
|
46
|
+
| 'config_audit' // config-audit.ts - Risky .env/config and debug flags
|
|
47
|
+
| 'file_flags' // file-flags.ts - Dangerous file patterns
|
|
48
|
+
| 'ai_comments' // comments.ts - AI comment patterns ("Generated by...")
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Layer 1 tier assignments
|
|
52
|
+
*
|
|
53
|
+
* Tier A (core):
|
|
54
|
+
* - known_secrets: Hardcoded secrets are objectively bad and high-impact
|
|
55
|
+
* - weak_crypto: Weak crypto is a classic SAST finding with clear remediation
|
|
56
|
+
* - sensitive_urls: Hardcoded webhook URLs + tokens are real data exfil vectors
|
|
57
|
+
*
|
|
58
|
+
* Tier B (ai_assisted):
|
|
59
|
+
* - entropy: Great at finding candidates, needs AI to separate real secrets from noise
|
|
60
|
+
* - config_audit: Depends on project norms; better reviewed with AI + project context
|
|
61
|
+
* - file_flags: Subjective items should be AI-triaged (except committed .env = Tier A)
|
|
62
|
+
*
|
|
63
|
+
* Tier C (experimental):
|
|
64
|
+
* - ai_comments: Not directly a vuln; belongs in separate "AI hygiene" report
|
|
65
|
+
*/
|
|
66
|
+
export const LAYER1_DETECTOR_TIERS: Record<Layer1DetectorName, DetectorTier> = {
|
|
67
|
+
known_secrets: 'core',
|
|
68
|
+
weak_crypto: 'core',
|
|
69
|
+
sensitive_urls: 'core',
|
|
70
|
+
entropy: 'ai_assisted',
|
|
71
|
+
config_audit: 'ai_assisted',
|
|
72
|
+
file_flags: 'ai_assisted', // Mixed: committed .env is effectively core
|
|
73
|
+
ai_comments: 'experimental',
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Mapping from vulnerability category to Layer 1 detector name
|
|
78
|
+
* Used for tier lookups when we only have the category
|
|
79
|
+
*/
|
|
80
|
+
export const LAYER1_CATEGORY_TO_DETECTOR: Partial<Record<VulnerabilityCategory, Layer1DetectorName>> = {
|
|
81
|
+
hardcoded_secret: 'known_secrets',
|
|
82
|
+
weak_crypto: 'weak_crypto',
|
|
83
|
+
sensitive_url: 'sensitive_urls',
|
|
84
|
+
high_entropy_string: 'entropy',
|
|
85
|
+
insecure_config: 'config_audit',
|
|
86
|
+
root_container: 'config_audit',
|
|
87
|
+
dangerous_file: 'file_flags',
|
|
88
|
+
ai_pattern: 'ai_comments', // AI comment patterns detected in Layer 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Layer 2 Detector Tier Mappings
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Layer 2 detector names (internal identifiers matching detector function names)
|
|
97
|
+
*/
|
|
98
|
+
export type Layer2DetectorName =
|
|
99
|
+
| 'dangerous_functions' // dangerous-functions.ts
|
|
100
|
+
| 'byok_patterns' // byok-patterns.ts
|
|
101
|
+
| 'ai_execution_sinks' // ai-execution-sinks.ts
|
|
102
|
+
| 'ai_agent_tools' // ai-agent-tools.ts
|
|
103
|
+
| 'auth_antipatterns' // auth-antipatterns.ts
|
|
104
|
+
| 'data_exposure' // data-exposure.ts
|
|
105
|
+
| 'ai_fingerprinting' // ai-fingerprinting.ts
|
|
106
|
+
| 'ai_prompt_hygiene' // ai-prompt-hygiene.ts
|
|
107
|
+
| 'logic_gates' // logic-gates.ts
|
|
108
|
+
| 'variables' // variables.ts
|
|
109
|
+
| 'risky_imports' // risky-imports.ts
|
|
110
|
+
| 'framework_checks' // framework-checks.ts
|
|
111
|
+
// M5: New AI-era detectors
|
|
112
|
+
| 'ai_rag_safety' // ai-rag-safety.ts - RAG data exfiltration
|
|
113
|
+
| 'ai_endpoint_protection' // ai-endpoint-protection.ts - Unprotected AI endpoints
|
|
114
|
+
| 'ai_schema_validation' // ai-schema-validation.ts - Schema mismatch
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Layer 2 tier assignments
|
|
118
|
+
*
|
|
119
|
+
* Tier A (core) - High-precision, high-risk, clear remediation:
|
|
120
|
+
* - dangerous_functions: Classic injection footholds (eval, exec, unsafe SQL)
|
|
121
|
+
* - byok_patterns: Storing/logging BYOK is critical AI-era customer trust risk
|
|
122
|
+
* - ai_execution_sinks: Core to Oculum's AI story; LLM output as code/commands
|
|
123
|
+
* - ai_agent_tools: Over-permissive tools are central AI alignment problem
|
|
124
|
+
*
|
|
125
|
+
* Tier B (ai_assisted) - Context-heavy, need AI validation:
|
|
126
|
+
* - auth_antipatterns: Very context-dependent; needs middleware awareness
|
|
127
|
+
* - data_exposure: Logging queries/user IDs is often acceptable but context-specific
|
|
128
|
+
* - ai_fingerprinting: TypeScript 'any' at trust boundaries is risk indicator, not vuln
|
|
129
|
+
* - ai_prompt_hygiene: Needs semantic understanding of prompt design
|
|
130
|
+
*
|
|
131
|
+
* Tier C (experimental) - High-noise, internal use only:
|
|
132
|
+
* - logic_gates: Many hits with limited actionable signal
|
|
133
|
+
* - variables: Generic token/password variable names - too generic
|
|
134
|
+
* - risky_imports: Most imports are fine; keep tiny whitelist if proved valuable
|
|
135
|
+
* - framework_checks: Many findings are style/low-risk; prone to noisy lint
|
|
136
|
+
*/
|
|
137
|
+
export const LAYER2_DETECTOR_TIERS: Record<Layer2DetectorName, DetectorTier> = {
|
|
138
|
+
// Tier A - Core SAST / AI-safety
|
|
139
|
+
dangerous_functions: 'core',
|
|
140
|
+
byok_patterns: 'core',
|
|
141
|
+
ai_execution_sinks: 'core',
|
|
142
|
+
ai_agent_tools: 'core',
|
|
143
|
+
|
|
144
|
+
// Tier B - AI-assisted heuristics
|
|
145
|
+
auth_antipatterns: 'ai_assisted',
|
|
146
|
+
data_exposure: 'ai_assisted',
|
|
147
|
+
ai_fingerprinting: 'ai_assisted',
|
|
148
|
+
ai_prompt_hygiene: 'ai_assisted',
|
|
149
|
+
|
|
150
|
+
// Tier C - Experimental / high-noise
|
|
151
|
+
logic_gates: 'experimental',
|
|
152
|
+
variables: 'experimental',
|
|
153
|
+
risky_imports: 'experimental',
|
|
154
|
+
framework_checks: 'experimental',
|
|
155
|
+
|
|
156
|
+
// M5: New AI-era detectors
|
|
157
|
+
ai_rag_safety: 'core', // Tier A - Cross-tenant data access is critical
|
|
158
|
+
ai_endpoint_protection: 'core', // Tier A - Cost abuse / API exposure has clear signals
|
|
159
|
+
ai_schema_validation: 'ai_assisted', // Tier B - Context-dependent, benefits from AI validation
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Mapping from vulnerability category to Layer 2 detector name
|
|
164
|
+
* Used for tier lookups when we only have the category
|
|
165
|
+
*
|
|
166
|
+
* NOTE: Some categories are used by multiple detectors:
|
|
167
|
+
* - ai_pattern: used by ai-fingerprinting (Tier B), byok-patterns (Tier A), dangerous-functions (Tier A)
|
|
168
|
+
* - insecure_config: used by config-audit (L1 Tier B), framework-checks (L2 Tier C)
|
|
169
|
+
*
|
|
170
|
+
* For ambiguous categories, we use the most conservative (highest trust) tier mapping.
|
|
171
|
+
* When category alone isn't sufficient, the orchestrator can use detector-specific tracking.
|
|
172
|
+
*/
|
|
173
|
+
export const LAYER2_CATEGORY_TO_DETECTOR: Partial<Record<VulnerabilityCategory, Layer2DetectorName>> = {
|
|
174
|
+
// Tier A categories (unambiguous)
|
|
175
|
+
dangerous_function: 'dangerous_functions',
|
|
176
|
+
sql_injection: 'dangerous_functions',
|
|
177
|
+
command_injection: 'dangerous_functions',
|
|
178
|
+
ai_unsafe_execution: 'ai_execution_sinks',
|
|
179
|
+
ai_overpermissive_tool: 'ai_agent_tools',
|
|
180
|
+
|
|
181
|
+
// Tier B categories
|
|
182
|
+
missing_auth: 'auth_antipatterns',
|
|
183
|
+
data_exposure: 'data_exposure',
|
|
184
|
+
ai_prompt_injection: 'ai_prompt_hygiene',
|
|
185
|
+
|
|
186
|
+
// ai_pattern is ambiguous - used by multiple detectors with different tiers:
|
|
187
|
+
// - ai-fingerprinting (Tier B): TypeScript 'any' at boundaries
|
|
188
|
+
// - byok-patterns (Tier A): BYOK key handling
|
|
189
|
+
// - dangerous-functions (Tier A): JSON.parse related AI patterns
|
|
190
|
+
// Default to ai_fingerprinting (Tier B) since it's most common; byok/dangerous_functions
|
|
191
|
+
// findings are usually categorized differently or handled explicitly
|
|
192
|
+
ai_pattern: 'ai_fingerprinting',
|
|
193
|
+
|
|
194
|
+
// Tier C categories
|
|
195
|
+
security_bypass: 'logic_gates',
|
|
196
|
+
sensitive_variable: 'variables',
|
|
197
|
+
suspicious_package: 'risky_imports',
|
|
198
|
+
cors_misconfiguration: 'framework_checks',
|
|
199
|
+
// insecure_config from framework-checks is Tier C in Layer 2
|
|
200
|
+
// (but same category from config-audit is Tier B in Layer 1 - handled by layer check)
|
|
201
|
+
insecure_config: 'framework_checks',
|
|
202
|
+
|
|
203
|
+
// M5: New AI-era categories
|
|
204
|
+
ai_rag_exfiltration: 'ai_rag_safety',
|
|
205
|
+
ai_endpoint_unprotected: 'ai_endpoint_protection',
|
|
206
|
+
ai_schema_mismatch: 'ai_schema_validation',
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================================================
|
|
210
|
+
// Tier Lookup Helpers
|
|
211
|
+
// ============================================================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get the tier for a vulnerability based on its category and layer
|
|
215
|
+
*/
|
|
216
|
+
export function getTierForCategory(
|
|
217
|
+
category: VulnerabilityCategory,
|
|
218
|
+
layer: 1 | 2 | 3
|
|
219
|
+
): DetectorTier {
|
|
220
|
+
if (layer === 1) {
|
|
221
|
+
const detector = LAYER1_CATEGORY_TO_DETECTOR[category]
|
|
222
|
+
if (detector) {
|
|
223
|
+
return LAYER1_DETECTOR_TIERS[detector]
|
|
224
|
+
}
|
|
225
|
+
} else if (layer === 2) {
|
|
226
|
+
const detector = LAYER2_CATEGORY_TO_DETECTOR[category]
|
|
227
|
+
if (detector) {
|
|
228
|
+
return LAYER2_DETECTOR_TIERS[detector]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Layer 3 findings are always core (AI semantic analysis)
|
|
233
|
+
if (layer === 3) {
|
|
234
|
+
return 'core'
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Default to ai_assisted if unmapped (safe default - will go through AI validation)
|
|
238
|
+
return 'ai_assisted'
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get tier for a Layer 1 detector by name
|
|
243
|
+
*/
|
|
244
|
+
export function getLayer1DetectorTier(detector: Layer1DetectorName): DetectorTier {
|
|
245
|
+
return LAYER1_DETECTOR_TIERS[detector]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get tier for a Layer 2 detector by name
|
|
250
|
+
*/
|
|
251
|
+
export function getLayer2DetectorTier(detector: Layer2DetectorName): DetectorTier {
|
|
252
|
+
return LAYER2_DETECTOR_TIERS[detector]
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check if a tier should be visible at a given scan depth
|
|
257
|
+
*/
|
|
258
|
+
export function isTierVisibleAtDepth(
|
|
259
|
+
tier: DetectorTier,
|
|
260
|
+
depth: 'cheap' | 'validated' | 'deep'
|
|
261
|
+
): boolean {
|
|
262
|
+
switch (depth) {
|
|
263
|
+
case 'cheap':
|
|
264
|
+
// Only Tier A (core) findings are visible in cheap scans
|
|
265
|
+
return tier === 'core'
|
|
266
|
+
case 'validated':
|
|
267
|
+
// Tier A always visible, Tier B visible after AI validation
|
|
268
|
+
return tier === 'core' || tier === 'ai_assisted'
|
|
269
|
+
case 'deep':
|
|
270
|
+
// Same as validated for visibility (deep adds Layer 3, not more tiers)
|
|
271
|
+
return tier === 'core' || tier === 'ai_assisted'
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if a tier should go through AI validation at a given scan depth
|
|
277
|
+
*/
|
|
278
|
+
export function shouldValidateWithAI(
|
|
279
|
+
tier: DetectorTier,
|
|
280
|
+
depth: 'cheap' | 'validated' | 'deep'
|
|
281
|
+
): boolean {
|
|
282
|
+
// Cheap scans skip AI validation entirely
|
|
283
|
+
if (depth === 'cheap') {
|
|
284
|
+
return false
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// In validated/deep, Tier B findings should go through AI validation
|
|
288
|
+
// Tier A is high-precision and doesn't need AI validation
|
|
289
|
+
// Tier C is hidden anyway
|
|
290
|
+
return tier === 'ai_assisted'
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Compute tier statistics from an array of vulnerabilities
|
|
295
|
+
*/
|
|
296
|
+
export function computeTierStats(
|
|
297
|
+
vulnerabilities: Array<{ category: VulnerabilityCategory; layer: 1 | 2 | 3 }>
|
|
298
|
+
): TierStats {
|
|
299
|
+
const stats: TierStats = {
|
|
300
|
+
core: 0,
|
|
301
|
+
ai_assisted: 0,
|
|
302
|
+
experimental: 0,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const vuln of vulnerabilities) {
|
|
306
|
+
const tier = getTierForCategory(vuln.category, vuln.layer)
|
|
307
|
+
stats[tier]++
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return stats
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Format tier stats for logging
|
|
315
|
+
*/
|
|
316
|
+
export function formatTierStats(stats: TierStats): string {
|
|
317
|
+
return `tiers={core:${stats.core},ai_assisted:${stats.ai_assisted},experimental:${stats.experimental}}`
|
|
318
|
+
}
|