@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,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Diff Parser
|
|
3
|
+
* Parses unified diff format to extract changed line ranges
|
|
4
|
+
* Used for incremental scanning to only flag findings on changed lines
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A hunk represents a contiguous block of changes
|
|
9
|
+
*/
|
|
10
|
+
export interface DiffHunk {
|
|
11
|
+
/** Starting line in the old file */
|
|
12
|
+
oldStart: number
|
|
13
|
+
/** Number of lines in old file */
|
|
14
|
+
oldLines: number
|
|
15
|
+
/** Starting line in the new file */
|
|
16
|
+
newStart: number
|
|
17
|
+
/** Number of lines in new file */
|
|
18
|
+
newLines: number
|
|
19
|
+
/** Lines that were added (line numbers in new file) */
|
|
20
|
+
addedLines: number[]
|
|
21
|
+
/** Lines that were removed (line numbers in old file) */
|
|
22
|
+
removedLines: number[]
|
|
23
|
+
/** Lines that were modified (context around changes in new file) */
|
|
24
|
+
contextLines: number[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parsed file diff
|
|
29
|
+
*/
|
|
30
|
+
export interface FileDiff {
|
|
31
|
+
/** File path (after rename if applicable) */
|
|
32
|
+
path: string
|
|
33
|
+
/** Old file path (if renamed) */
|
|
34
|
+
oldPath?: string
|
|
35
|
+
/** Whether the file was added */
|
|
36
|
+
isNew: boolean
|
|
37
|
+
/** Whether the file was deleted */
|
|
38
|
+
isDeleted: boolean
|
|
39
|
+
/** Whether the file was renamed */
|
|
40
|
+
isRenamed: boolean
|
|
41
|
+
/** Hunks containing the actual changes */
|
|
42
|
+
hunks: DiffHunk[]
|
|
43
|
+
/** All changed line numbers in the new file */
|
|
44
|
+
changedLines: Set<number>
|
|
45
|
+
/** All lines near changes (within context window) */
|
|
46
|
+
affectedLines: Set<number>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Parse a unified diff output
|
|
51
|
+
*
|
|
52
|
+
* @param diffOutput - The raw git diff output
|
|
53
|
+
* @param contextWindow - Lines around changes to consider "affected" (default: 5)
|
|
54
|
+
* @returns Map of file path to FileDiff
|
|
55
|
+
*/
|
|
56
|
+
export function parseDiff(diffOutput: string, contextWindow: number = 5): Map<string, FileDiff> {
|
|
57
|
+
const files = new Map<string, FileDiff>()
|
|
58
|
+
|
|
59
|
+
if (!diffOutput || diffOutput.trim() === '') {
|
|
60
|
+
return files
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Split into file sections (each starts with "diff --git")
|
|
64
|
+
const fileSections = diffOutput.split(/^diff --git /gm).filter(s => s.trim())
|
|
65
|
+
|
|
66
|
+
for (const section of fileSections) {
|
|
67
|
+
const fileDiff = parseFileSection('diff --git ' + section, contextWindow)
|
|
68
|
+
if (fileDiff) {
|
|
69
|
+
files.set(fileDiff.path, fileDiff)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return files
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse a single file's diff section
|
|
78
|
+
*/
|
|
79
|
+
function parseFileSection(section: string, contextWindow: number): FileDiff | null {
|
|
80
|
+
const lines = section.split('\n')
|
|
81
|
+
|
|
82
|
+
// Extract file paths from header
|
|
83
|
+
// Format: diff --git a/path/to/file b/path/to/file
|
|
84
|
+
const headerMatch = lines[0].match(/diff --git a\/(.+) b\/(.+)/)
|
|
85
|
+
if (!headerMatch) return null
|
|
86
|
+
|
|
87
|
+
const oldPath = headerMatch[1]
|
|
88
|
+
const newPath = headerMatch[2]
|
|
89
|
+
|
|
90
|
+
// Detect file status
|
|
91
|
+
let isNew = false
|
|
92
|
+
let isDeleted = false
|
|
93
|
+
let isRenamed = oldPath !== newPath
|
|
94
|
+
|
|
95
|
+
for (const line of lines.slice(0, 10)) {
|
|
96
|
+
if (line.startsWith('new file mode')) isNew = true
|
|
97
|
+
if (line.startsWith('deleted file mode')) isDeleted = true
|
|
98
|
+
if (line.startsWith('rename from')) isRenamed = true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Parse hunks
|
|
102
|
+
const hunks: DiffHunk[] = []
|
|
103
|
+
const changedLines = new Set<number>()
|
|
104
|
+
const affectedLines = new Set<number>()
|
|
105
|
+
|
|
106
|
+
// Find hunk headers: @@ -oldStart,oldLines +newStart,newLines @@
|
|
107
|
+
const hunkRegex = /@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/g
|
|
108
|
+
let match: RegExpExecArray | null
|
|
109
|
+
|
|
110
|
+
const sectionContent = section
|
|
111
|
+
|
|
112
|
+
while ((match = hunkRegex.exec(sectionContent)) !== null) {
|
|
113
|
+
const oldStart = parseInt(match[1], 10)
|
|
114
|
+
const oldLines = parseInt(match[2] || '1', 10)
|
|
115
|
+
const newStart = parseInt(match[3], 10)
|
|
116
|
+
const newLines = parseInt(match[4] || '1', 10)
|
|
117
|
+
|
|
118
|
+
// Find the content of this hunk (until next @@ or end)
|
|
119
|
+
const hunkStartIndex = match.index + match[0].length
|
|
120
|
+
const nextHunkMatch = /@@ -\d+/.exec(sectionContent.slice(hunkStartIndex + 1))
|
|
121
|
+
const hunkEndIndex = nextHunkMatch
|
|
122
|
+
? hunkStartIndex + 1 + nextHunkMatch.index
|
|
123
|
+
: sectionContent.length
|
|
124
|
+
|
|
125
|
+
const hunkContent = sectionContent.slice(hunkStartIndex, hunkEndIndex)
|
|
126
|
+
const hunkLines = hunkContent.split('\n')
|
|
127
|
+
|
|
128
|
+
const addedLines: number[] = []
|
|
129
|
+
const removedLines: number[] = []
|
|
130
|
+
const contextLines: number[] = []
|
|
131
|
+
|
|
132
|
+
let newLineNum = newStart
|
|
133
|
+
let oldLineNum = oldStart
|
|
134
|
+
|
|
135
|
+
for (const line of hunkLines) {
|
|
136
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
137
|
+
// Added line
|
|
138
|
+
addedLines.push(newLineNum)
|
|
139
|
+
changedLines.add(newLineNum)
|
|
140
|
+
newLineNum++
|
|
141
|
+
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
142
|
+
// Removed line
|
|
143
|
+
removedLines.push(oldLineNum)
|
|
144
|
+
oldLineNum++
|
|
145
|
+
} else if (line.startsWith(' ') || line === '') {
|
|
146
|
+
// Context line
|
|
147
|
+
contextLines.push(newLineNum)
|
|
148
|
+
newLineNum++
|
|
149
|
+
oldLineNum++
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
hunks.push({
|
|
154
|
+
oldStart,
|
|
155
|
+
oldLines,
|
|
156
|
+
newStart,
|
|
157
|
+
newLines,
|
|
158
|
+
addedLines,
|
|
159
|
+
removedLines,
|
|
160
|
+
contextLines,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Calculate affected lines (changed lines + context window)
|
|
165
|
+
for (const line of changedLines) {
|
|
166
|
+
for (let i = Math.max(1, line - contextWindow); i <= line + contextWindow; i++) {
|
|
167
|
+
affectedLines.add(i)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
path: newPath,
|
|
173
|
+
oldPath: isRenamed ? oldPath : undefined,
|
|
174
|
+
isNew,
|
|
175
|
+
isDeleted,
|
|
176
|
+
isRenamed,
|
|
177
|
+
hunks,
|
|
178
|
+
changedLines,
|
|
179
|
+
affectedLines,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Parse a simple list of changed file paths
|
|
185
|
+
* Used when full diff isn't available (e.g., GitHub API file list)
|
|
186
|
+
*/
|
|
187
|
+
export function parseChangedFileList(files: string[]): Map<string, FileDiff> {
|
|
188
|
+
const result = new Map<string, FileDiff>()
|
|
189
|
+
|
|
190
|
+
for (const path of files) {
|
|
191
|
+
result.set(path, {
|
|
192
|
+
path,
|
|
193
|
+
isNew: false,
|
|
194
|
+
isDeleted: false,
|
|
195
|
+
isRenamed: false,
|
|
196
|
+
hunks: [],
|
|
197
|
+
// Without diff content, consider all lines as changed
|
|
198
|
+
changedLines: new Set(),
|
|
199
|
+
affectedLines: new Set(),
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if a finding is on a changed line
|
|
208
|
+
*/
|
|
209
|
+
export function isOnChangedLine(
|
|
210
|
+
filePath: string,
|
|
211
|
+
lineNumber: number,
|
|
212
|
+
diffs: Map<string, FileDiff>
|
|
213
|
+
): boolean {
|
|
214
|
+
const fileDiff = diffs.get(filePath)
|
|
215
|
+
if (!fileDiff) return false
|
|
216
|
+
|
|
217
|
+
// If no specific changed lines (file list only), consider all findings relevant
|
|
218
|
+
if (fileDiff.changedLines.size === 0) return true
|
|
219
|
+
|
|
220
|
+
return fileDiff.changedLines.has(lineNumber)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if a finding is near a changed line (within context window)
|
|
225
|
+
*/
|
|
226
|
+
export function isNearChangedLine(
|
|
227
|
+
filePath: string,
|
|
228
|
+
lineNumber: number,
|
|
229
|
+
diffs: Map<string, FileDiff>
|
|
230
|
+
): boolean {
|
|
231
|
+
const fileDiff = diffs.get(filePath)
|
|
232
|
+
if (!fileDiff) return false
|
|
233
|
+
|
|
234
|
+
// If no specific affected lines, consider all findings relevant
|
|
235
|
+
if (fileDiff.affectedLines.size === 0) return true
|
|
236
|
+
|
|
237
|
+
return fileDiff.affectedLines.has(lineNumber)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get all changed file paths from diffs
|
|
242
|
+
*/
|
|
243
|
+
export function getChangedFilePaths(diffs: Map<string, FileDiff>): string[] {
|
|
244
|
+
return Array.from(diffs.keys()).filter(path => {
|
|
245
|
+
const diff = diffs.get(path)
|
|
246
|
+
return diff && !diff.isDeleted
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Filter vulnerabilities to only those on/near changed lines
|
|
252
|
+
*/
|
|
253
|
+
export function filterToChangedLines<T extends { filePath: string; lineNumber: number }>(
|
|
254
|
+
findings: T[],
|
|
255
|
+
diffs: Map<string, FileDiff>,
|
|
256
|
+
options: { strictMode?: boolean } = {}
|
|
257
|
+
): T[] {
|
|
258
|
+
const { strictMode = false } = options
|
|
259
|
+
|
|
260
|
+
return findings.filter(finding => {
|
|
261
|
+
// If file not in diff, it wasn't changed - exclude
|
|
262
|
+
if (!diffs.has(finding.filePath)) return false
|
|
263
|
+
|
|
264
|
+
// In strict mode, only include findings on exactly changed lines
|
|
265
|
+
if (strictMode) {
|
|
266
|
+
return isOnChangedLine(finding.filePath, finding.lineNumber, diffs)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// In normal mode, include findings near changed lines
|
|
270
|
+
return isNearChangedLine(finding.filePath, finding.lineNumber, diffs)
|
|
271
|
+
})
|
|
272
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Imported Auth Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects auth middleware/helpers imported from other files to avoid
|
|
5
|
+
* false positives on routes that are actually protected via imports.
|
|
6
|
+
*
|
|
7
|
+
* Example: A route file that does `import { authMiddleware } from '@/lib/auth'`
|
|
8
|
+
* and wraps handlers with it should not be flagged as "missing auth".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ScanFile } from '../types'
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
export interface ImportedAuthInfo {
|
|
18
|
+
importPath: string // '@/lib/auth' or './middleware'
|
|
19
|
+
importedNames: string[] // ['authMiddleware', 'requireAuth']
|
|
20
|
+
isAuthRelated: boolean // Based on name patterns
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface FileAuthImports {
|
|
24
|
+
filePath: string
|
|
25
|
+
imports: ImportedAuthInfo[]
|
|
26
|
+
usesImportedAuth: boolean // Does the file actually use imported auth?
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Patterns
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Patterns that indicate auth-related imports by name
|
|
35
|
+
*/
|
|
36
|
+
const AUTH_NAME_PATTERNS = [
|
|
37
|
+
/auth/i,
|
|
38
|
+
/middleware/i,
|
|
39
|
+
/protect/i,
|
|
40
|
+
/guard/i,
|
|
41
|
+
/session/i,
|
|
42
|
+
/verify/i,
|
|
43
|
+
/secure/i,
|
|
44
|
+
/jwt/i,
|
|
45
|
+
/token/i,
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Specific auth import names commonly used
|
|
50
|
+
*/
|
|
51
|
+
const KNOWN_AUTH_IMPORTS = new Set([
|
|
52
|
+
// Next.js / NextAuth
|
|
53
|
+
'auth',
|
|
54
|
+
'getServerSession',
|
|
55
|
+
'getSession',
|
|
56
|
+
'withAuth',
|
|
57
|
+
'getToken',
|
|
58
|
+
'NextAuth',
|
|
59
|
+
'NextAuthOptions',
|
|
60
|
+
|
|
61
|
+
// Clerk
|
|
62
|
+
'auth',
|
|
63
|
+
'currentUser',
|
|
64
|
+
'clerkMiddleware',
|
|
65
|
+
'getAuth',
|
|
66
|
+
'SignedIn',
|
|
67
|
+
'SignedOut',
|
|
68
|
+
|
|
69
|
+
// Auth0
|
|
70
|
+
'withPageAuthRequired',
|
|
71
|
+
'withApiAuthRequired',
|
|
72
|
+
'getSession',
|
|
73
|
+
'getAccessToken',
|
|
74
|
+
|
|
75
|
+
// Custom auth patterns
|
|
76
|
+
'authMiddleware',
|
|
77
|
+
'requireAuth',
|
|
78
|
+
'requireAuthentication',
|
|
79
|
+
'checkAuth',
|
|
80
|
+
'verifyAuth',
|
|
81
|
+
'protectRoute',
|
|
82
|
+
'withAuthentication',
|
|
83
|
+
'authenticated',
|
|
84
|
+
'getCurrentUser',
|
|
85
|
+
'getCurrentUserId',
|
|
86
|
+
'getUser',
|
|
87
|
+
'getUserId',
|
|
88
|
+
])
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Patterns indicating auth-related import paths
|
|
92
|
+
*/
|
|
93
|
+
const AUTH_PATH_PATTERNS = [
|
|
94
|
+
/\/auth\//i,
|
|
95
|
+
/\/middleware/i,
|
|
96
|
+
/\/lib\/auth/i,
|
|
97
|
+
/\/utils\/auth/i,
|
|
98
|
+
/\/helpers\/auth/i,
|
|
99
|
+
/next-auth/i,
|
|
100
|
+
/@clerk/i,
|
|
101
|
+
/@auth0/i,
|
|
102
|
+
/lucia/i,
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Import Extraction
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Extract all imports from a file's content
|
|
111
|
+
*/
|
|
112
|
+
export function extractImports(content: string): ImportedAuthInfo[] {
|
|
113
|
+
const imports: ImportedAuthInfo[] = []
|
|
114
|
+
|
|
115
|
+
// ES6 named imports: import { x, y } from 'path'
|
|
116
|
+
const es6NamedPattern = /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g
|
|
117
|
+
let match
|
|
118
|
+
|
|
119
|
+
while ((match = es6NamedPattern.exec(content)) !== null) {
|
|
120
|
+
const names = match[1]
|
|
121
|
+
.split(',')
|
|
122
|
+
.map(n => n.trim().split(/\s+as\s+/)[0].trim()) // Handle 'x as y'
|
|
123
|
+
.filter(n => n.length > 0)
|
|
124
|
+
const importPath = match[2]
|
|
125
|
+
|
|
126
|
+
const isAuthRelated = isAuthRelatedImport(names, importPath)
|
|
127
|
+
|
|
128
|
+
imports.push({
|
|
129
|
+
importPath,
|
|
130
|
+
importedNames: names,
|
|
131
|
+
isAuthRelated,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ES6 default imports: import x from 'path'
|
|
136
|
+
const defaultPattern = /import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g
|
|
137
|
+
while ((match = defaultPattern.exec(content)) !== null) {
|
|
138
|
+
const name = match[1]
|
|
139
|
+
const importPath = match[2]
|
|
140
|
+
|
|
141
|
+
// Skip if already captured as named import
|
|
142
|
+
if (imports.some(imp => imp.importPath === importPath)) continue
|
|
143
|
+
|
|
144
|
+
const isAuthRelated = isAuthRelatedImport([name], importPath)
|
|
145
|
+
|
|
146
|
+
imports.push({
|
|
147
|
+
importPath,
|
|
148
|
+
importedNames: [name],
|
|
149
|
+
isAuthRelated,
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// CommonJS require: const { x } = require('path')
|
|
154
|
+
const requireDestructurePattern = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
155
|
+
while ((match = requireDestructurePattern.exec(content)) !== null) {
|
|
156
|
+
const names = match[1]
|
|
157
|
+
.split(',')
|
|
158
|
+
.map(n => n.trim().split(/\s*:\s*/)[0].trim()) // Handle 'x: y' renaming
|
|
159
|
+
.filter(n => n.length > 0)
|
|
160
|
+
const importPath = match[2]
|
|
161
|
+
|
|
162
|
+
const isAuthRelated = isAuthRelatedImport(names, importPath)
|
|
163
|
+
|
|
164
|
+
imports.push({
|
|
165
|
+
importPath,
|
|
166
|
+
importedNames: names,
|
|
167
|
+
isAuthRelated,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// CommonJS require: const x = require('path')
|
|
172
|
+
const requireDefaultPattern = /(?:const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
173
|
+
while ((match = requireDefaultPattern.exec(content)) !== null) {
|
|
174
|
+
const name = match[1]
|
|
175
|
+
const importPath = match[2]
|
|
176
|
+
|
|
177
|
+
// Skip if already captured
|
|
178
|
+
if (imports.some(imp => imp.importPath === importPath)) continue
|
|
179
|
+
|
|
180
|
+
const isAuthRelated = isAuthRelatedImport([name], importPath)
|
|
181
|
+
|
|
182
|
+
imports.push({
|
|
183
|
+
importPath,
|
|
184
|
+
importedNames: [name],
|
|
185
|
+
isAuthRelated,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return imports
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if an import is auth-related based on names and path
|
|
194
|
+
*/
|
|
195
|
+
function isAuthRelatedImport(names: string[], importPath: string): boolean {
|
|
196
|
+
// Check if path is auth-related
|
|
197
|
+
if (AUTH_PATH_PATTERNS.some(p => p.test(importPath))) {
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Check if any imported name is auth-related
|
|
202
|
+
for (const name of names) {
|
|
203
|
+
// Known auth imports
|
|
204
|
+
if (KNOWN_AUTH_IMPORTS.has(name)) {
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Pattern-based detection
|
|
209
|
+
if (AUTH_NAME_PATTERNS.some(p => p.test(name))) {
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// Auth Usage Detection
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Check if imported auth is actually used to protect routes/handlers
|
|
223
|
+
*/
|
|
224
|
+
export function detectImportedAuthUsage(
|
|
225
|
+
content: string,
|
|
226
|
+
authImports: ImportedAuthInfo[]
|
|
227
|
+
): boolean {
|
|
228
|
+
if (authImports.length === 0) return false
|
|
229
|
+
|
|
230
|
+
const authNames = authImports.flatMap(imp => imp.importedNames)
|
|
231
|
+
|
|
232
|
+
for (const name of authNames) {
|
|
233
|
+
// Pattern 1: Handler wrapping - export const GET = authMiddleware(...)
|
|
234
|
+
const wrapperPattern = new RegExp(
|
|
235
|
+
`(?:export\\s+(?:const|function)\\s+(?:GET|POST|PUT|PATCH|DELETE|handler)\\s*=\\s*${escapeRegex(name)}\\s*\\()` +
|
|
236
|
+
`|(?:${escapeRegex(name)}\\s*\\(\\s*(?:async\\s+)?(?:function|\\())`
|
|
237
|
+
)
|
|
238
|
+
if (wrapperPattern.test(content)) {
|
|
239
|
+
return true
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Pattern 2: Middleware chain - app.use(authMiddleware)
|
|
243
|
+
const middlewareChainPattern = new RegExp(
|
|
244
|
+
`\\.(?:use|all|get|post|put|patch|delete)\\s*\\([^)]*${escapeRegex(name)}`
|
|
245
|
+
)
|
|
246
|
+
if (middlewareChainPattern.test(content)) {
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Pattern 3: Express/Fastify route with middleware - router.get('/', requireAuth, handler)
|
|
251
|
+
const routeMiddlewarePattern = new RegExp(
|
|
252
|
+
`\\.(?:get|post|put|patch|delete)\\s*\\([^,]+,\\s*${escapeRegex(name)}`
|
|
253
|
+
)
|
|
254
|
+
if (routeMiddlewarePattern.test(content)) {
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Pattern 4: HOC pattern - withAuth(Component)
|
|
259
|
+
const hocPattern = new RegExp(`${escapeRegex(name)}\\s*\\(\\s*\\w+\\s*\\)`)
|
|
260
|
+
if (hocPattern.test(content)) {
|
|
261
|
+
return true
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Pattern 5: Auth function call at top of handler - const session = await auth()
|
|
265
|
+
const authCallPattern = new RegExp(
|
|
266
|
+
`(?:await\\s+)?${escapeRegex(name)}\\s*\\(\\s*\\)` +
|
|
267
|
+
`|${escapeRegex(name)}\\s*\\(\\s*(?:req|request|ctx|context)\\s*[,)]`
|
|
268
|
+
)
|
|
269
|
+
if (authCallPattern.test(content)) {
|
|
270
|
+
return true
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return false
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Escape special regex characters in a string
|
|
279
|
+
*/
|
|
280
|
+
function escapeRegex(str: string): string {
|
|
281
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// Registry Builder
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build a registry of files and their auth imports
|
|
290
|
+
*/
|
|
291
|
+
export function buildFileAuthImports(files: ScanFile[]): Map<string, FileAuthImports> {
|
|
292
|
+
const registry = new Map<string, FileAuthImports>()
|
|
293
|
+
|
|
294
|
+
for (const file of files) {
|
|
295
|
+
const allImports = extractImports(file.content)
|
|
296
|
+
const authImports = allImports.filter(imp => imp.isAuthRelated)
|
|
297
|
+
|
|
298
|
+
const usesImportedAuth =
|
|
299
|
+
authImports.length > 0 &&
|
|
300
|
+
detectImportedAuthUsage(file.content, authImports)
|
|
301
|
+
|
|
302
|
+
registry.set(file.path, {
|
|
303
|
+
filePath: file.path,
|
|
304
|
+
imports: authImports,
|
|
305
|
+
usesImportedAuth,
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return registry
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Check if a specific file uses imported auth protection
|
|
314
|
+
*/
|
|
315
|
+
export function fileUsesImportedAuth(
|
|
316
|
+
filePath: string,
|
|
317
|
+
registry: Map<string, FileAuthImports>
|
|
318
|
+
): boolean {
|
|
319
|
+
return registry.get(filePath)?.usesImportedAuth ?? false
|
|
320
|
+
}
|