@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,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Context Helpers
|
|
3
|
+
* Centralized utility functions for detecting file and code context
|
|
4
|
+
* Used across Layer 1 and Layer 2 scanners to reduce false positives
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// File Path Context Detection
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if file is server-only (not bundled to client)
|
|
13
|
+
* Server-only files can safely use service role keys and other admin secrets
|
|
14
|
+
*/
|
|
15
|
+
export function isServerOnlyFile(filePath: string): boolean {
|
|
16
|
+
const serverPatterns = [
|
|
17
|
+
/lib\/supabase\/(server|admin|middleware)\.(ts|js)$/i,
|
|
18
|
+
/\/api\//i, // Next.js API routes
|
|
19
|
+
/\/server\//i, // Server directories
|
|
20
|
+
/\.server\.(ts|js|tsx|jsx)$/i, // .server.ts files
|
|
21
|
+
/\/actions\//i, // Server actions
|
|
22
|
+
/middleware\.(ts|js)$/i, // Middleware files
|
|
23
|
+
/\/cron\//i, // Cron jobs
|
|
24
|
+
/\/workers?\//i, // Worker files
|
|
25
|
+
/\/scripts?\//i, // Scripts
|
|
26
|
+
/\/seed\//i, // Database seeds
|
|
27
|
+
/\/migrations?\//i, // Database migrations
|
|
28
|
+
/\/lib\/[^/]+\/server/i, // lib/*/server patterns
|
|
29
|
+
/\/utils\/server/i, // utils/server
|
|
30
|
+
/\/helpers\/server/i, // helpers/server
|
|
31
|
+
/\.action\.(ts|js)$/i, // .action.ts files
|
|
32
|
+
/route\.(ts|js)$/i, // Next.js route handlers
|
|
33
|
+
]
|
|
34
|
+
return serverPatterns.some(pattern => pattern.test(filePath))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if file is a test, mock, or fixture file
|
|
39
|
+
* These files often contain fake secrets and should have lower severity
|
|
40
|
+
*/
|
|
41
|
+
export function isTestOrMockFile(filePath: string): boolean {
|
|
42
|
+
const testPatterns = [
|
|
43
|
+
/\.(test|spec)\.(ts|tsx|js|jsx)$/i,
|
|
44
|
+
/\/__tests__\//i,
|
|
45
|
+
/\/test\//i,
|
|
46
|
+
/\/tests\//i,
|
|
47
|
+
/\/mock/i,
|
|
48
|
+
/\/mocks\//i,
|
|
49
|
+
/\/fixtures?\//i,
|
|
50
|
+
/\.mock\.(ts|tsx|js|jsx)$/i,
|
|
51
|
+
/\.stub\.(ts|tsx|js|jsx)$/i,
|
|
52
|
+
/\.(stories|story)\.(ts|tsx|js|jsx)$/i, // Storybook
|
|
53
|
+
/\/e2e\//i, // E2E tests
|
|
54
|
+
/\/cypress\//i, // Cypress tests
|
|
55
|
+
/\/playwright\//i, // Playwright tests
|
|
56
|
+
/\/vitest\//i, // Vitest
|
|
57
|
+
/\/jest\//i, // Jest
|
|
58
|
+
]
|
|
59
|
+
return testPatterns.some(pattern => pattern.test(filePath))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if file is an example/sample/template file
|
|
64
|
+
* These files should be skipped or have significantly reduced severity
|
|
65
|
+
*/
|
|
66
|
+
export function isExampleFile(filePath: string): boolean {
|
|
67
|
+
return (
|
|
68
|
+
filePath.includes('.example') ||
|
|
69
|
+
filePath.includes('.sample') ||
|
|
70
|
+
filePath.includes('.template') ||
|
|
71
|
+
filePath.includes('README') ||
|
|
72
|
+
filePath.includes('/examples/') ||
|
|
73
|
+
filePath.includes('/example/') ||
|
|
74
|
+
filePath.includes('/demo/') ||
|
|
75
|
+
filePath.includes('/demos/')
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if file is in an examples/demo directory
|
|
81
|
+
* Stronger check than isExampleFile - specifically for directories
|
|
82
|
+
* These are typically tutorial/demo code, not production patterns
|
|
83
|
+
*/
|
|
84
|
+
export function isExampleDirectory(filePath: string): boolean {
|
|
85
|
+
const examplePatterns = [
|
|
86
|
+
/\/examples?\//i,
|
|
87
|
+
/\/demos?\//i,
|
|
88
|
+
/\/templates?\//i,
|
|
89
|
+
/\/samples?\//i,
|
|
90
|
+
/\/tutorials?\//i,
|
|
91
|
+
/\/cookbook\//i,
|
|
92
|
+
/\/quickstart\//i,
|
|
93
|
+
/\/getting-started\//i,
|
|
94
|
+
]
|
|
95
|
+
return examplePatterns.some(pattern => pattern.test(filePath))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if file is library/framework code (base classes, utilities)
|
|
100
|
+
* Library code is intentionally generic - consumers add security
|
|
101
|
+
* This applies to: langchain, vercel/ai, llamaindex, etc.
|
|
102
|
+
*/
|
|
103
|
+
export function isLibraryCode(filePath: string): boolean {
|
|
104
|
+
const libraryPatterns = [
|
|
105
|
+
// Package source directories in monorepos
|
|
106
|
+
/\/libs\/[^/]+\/src\//i,
|
|
107
|
+
/\/packages\/[^/]+\/src\//i,
|
|
108
|
+
// Common library patterns
|
|
109
|
+
/\/langchain-/i,
|
|
110
|
+
/\/llamaindex/i,
|
|
111
|
+
// Source files that aren't examples or tests
|
|
112
|
+
/\/src\/(?!.*(?:examples?|demos?|tests?)\/).*\.(ts|js)$/i,
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
// Must match library pattern AND not be example/test
|
|
116
|
+
return (
|
|
117
|
+
libraryPatterns.some(pattern => pattern.test(filePath)) &&
|
|
118
|
+
!isExampleDirectory(filePath) &&
|
|
119
|
+
!isTestOrMockFile(filePath)
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Check if file is a fixture file (test data, mock responses)
|
|
125
|
+
* Fixtures contain fake data and should have reduced severity
|
|
126
|
+
*/
|
|
127
|
+
export function isFixtureFile(filePath: string): boolean {
|
|
128
|
+
const fixturePatterns = [
|
|
129
|
+
/__fixtures__\//i,
|
|
130
|
+
/\.fixture\./i,
|
|
131
|
+
/fixtures?\//i,
|
|
132
|
+
/testdata\//i,
|
|
133
|
+
/test-data\//i,
|
|
134
|
+
/test_data\//i,
|
|
135
|
+
/mock-data\//i,
|
|
136
|
+
/mockdata\//i,
|
|
137
|
+
/\.mock\./i,
|
|
138
|
+
/\.stub\./i,
|
|
139
|
+
]
|
|
140
|
+
return fixturePatterns.some(pattern => pattern.test(filePath))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check if file is documentation (README, CHANGELOG, etc.)
|
|
145
|
+
* These files should typically be skipped for security scanning
|
|
146
|
+
*/
|
|
147
|
+
export function isDocumentationFile(filePath: string): boolean {
|
|
148
|
+
const docPatterns = [
|
|
149
|
+
/README/i,
|
|
150
|
+
/CHANGELOG/i,
|
|
151
|
+
/CONTRIBUTING/i,
|
|
152
|
+
/LICENSE/i,
|
|
153
|
+
/\.md$/i,
|
|
154
|
+
/\.mdx$/i,
|
|
155
|
+
/\/docs\//i,
|
|
156
|
+
/\/documentation\//i,
|
|
157
|
+
]
|
|
158
|
+
return docPatterns.some(pattern => pattern.test(filePath))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check if file is scanner code, fixture, or rule definition
|
|
163
|
+
* Avoid flagging the scanner's own code/test cases
|
|
164
|
+
*/
|
|
165
|
+
export function isScannerOrFixtureFile(filePath: string): boolean {
|
|
166
|
+
const scannerPatterns = [
|
|
167
|
+
/\/scanner\//i,
|
|
168
|
+
/\/detector\//i,
|
|
169
|
+
/\/security\//i,
|
|
170
|
+
/\/rules?\//i,
|
|
171
|
+
/\/patterns?\//i,
|
|
172
|
+
/\/fixtures?\//i,
|
|
173
|
+
/\/testdata\//i,
|
|
174
|
+
/\/test-data\//i,
|
|
175
|
+
/\/test_data\//i,
|
|
176
|
+
]
|
|
177
|
+
return scannerPatterns.some(pattern => pattern.test(filePath))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if file is likely client-bundled (exposed to browser)
|
|
182
|
+
*/
|
|
183
|
+
export function isClientBundledFile(filePath: string): boolean {
|
|
184
|
+
// Files in these locations are typically client-bundled
|
|
185
|
+
const clientPatterns = [
|
|
186
|
+
/\/components\//i,
|
|
187
|
+
/\/pages\//i, // Next.js pages (can be SSR, but code visible)
|
|
188
|
+
/\/app\/.*page\.(ts|tsx|js|jsx)$/i, // Next.js app router pages
|
|
189
|
+
/\/hooks\//i,
|
|
190
|
+
/\/contexts?\//i,
|
|
191
|
+
/\/providers?\//i,
|
|
192
|
+
/\/stores?\//i, // State management
|
|
193
|
+
/\.client\.(ts|js|tsx|jsx)$/i, // .client.ts files
|
|
194
|
+
]
|
|
195
|
+
|
|
196
|
+
// But not if they're also server files
|
|
197
|
+
if (isServerOnlyFile(filePath)) {
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return clientPatterns.some(pattern => pattern.test(filePath))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// Code Line Context Detection
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if line uses environment variable reference (not hardcoded)
|
|
210
|
+
*/
|
|
211
|
+
export function isEnvVarReference(line: string): boolean {
|
|
212
|
+
return (
|
|
213
|
+
/process\.env\.[A-Z_]+/.test(line) ||
|
|
214
|
+
/\$\{?[A-Z_]+\}?/.test(line) ||
|
|
215
|
+
/import\.meta\.env\.[A-Z_]+/.test(line) ||
|
|
216
|
+
/Deno\.env\.get\(/.test(line) ||
|
|
217
|
+
/os\.environ\[/.test(line) || // Python
|
|
218
|
+
/os\.getenv\(/.test(line) || // Python
|
|
219
|
+
/ENV\[['"]/.test(line) || // Ruby
|
|
220
|
+
/env\(["']/.test(line) // Laravel PHP
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if line uses NEXT_PUBLIC_ prefix (client-exposed)
|
|
226
|
+
*/
|
|
227
|
+
export function isNextPublicEnvVar(line: string): boolean {
|
|
228
|
+
return /NEXT_PUBLIC_[A-Z_]+/.test(line)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check if line is a comment
|
|
233
|
+
*/
|
|
234
|
+
export function isComment(lineContent: string): boolean {
|
|
235
|
+
const trimmed = lineContent.trim()
|
|
236
|
+
return (
|
|
237
|
+
trimmed.startsWith('//') ||
|
|
238
|
+
trimmed.startsWith('#') ||
|
|
239
|
+
trimmed.startsWith('*') ||
|
|
240
|
+
trimmed.startsWith('/*') ||
|
|
241
|
+
trimmed.startsWith('"""') ||
|
|
242
|
+
trimmed.startsWith("'''") ||
|
|
243
|
+
trimmed.startsWith('<!--')
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Check if value/line appears to be a placeholder
|
|
249
|
+
*/
|
|
250
|
+
export function isPlaceholderValue(value: string, line: string): boolean {
|
|
251
|
+
const placeholderPatterns = [
|
|
252
|
+
/xxx/i,
|
|
253
|
+
/your[-_]?/i,
|
|
254
|
+
/YOUR[-_]?/i,
|
|
255
|
+
/placeholder/i,
|
|
256
|
+
/example/i,
|
|
257
|
+
/REPLACE[-_]?/i,
|
|
258
|
+
/CHANGEME/i,
|
|
259
|
+
/<[a-z_-]+>/i, // <your-api-key>
|
|
260
|
+
/\[\s*[a-z_-]+\s*\]/i, // [API_KEY]
|
|
261
|
+
/todo/i,
|
|
262
|
+
/fixme/i,
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
return placeholderPatterns.some(pattern =>
|
|
266
|
+
pattern.test(value) || pattern.test(line)
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================================
|
|
271
|
+
// Security Context Detection
|
|
272
|
+
// ============================================================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if line/path indicates a public endpoint (health, webhook, cron)
|
|
276
|
+
* These don't need authentication
|
|
277
|
+
*/
|
|
278
|
+
export function isPublicEndpoint(lineContent: string, filePath: string): boolean {
|
|
279
|
+
// Health check patterns
|
|
280
|
+
const healthCheckPatterns = [
|
|
281
|
+
/\/health\/?["'`]?/i,
|
|
282
|
+
/\/healthz\/?["'`]?/i,
|
|
283
|
+
/\/ready\/?["'`]?/i,
|
|
284
|
+
/\/readyz\/?["'`]?/i,
|
|
285
|
+
/\/live\/?["'`]?/i,
|
|
286
|
+
/\/livez\/?["'`]?/i,
|
|
287
|
+
/\/ping\/?["'`]?/i,
|
|
288
|
+
/\/status\/?["'`]?/i,
|
|
289
|
+
/\/api\/health/i,
|
|
290
|
+
/\/api\/status/i,
|
|
291
|
+
/\/_health/i,
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
// Webhook patterns
|
|
295
|
+
const webhookPatterns = [
|
|
296
|
+
/\/webhook/i,
|
|
297
|
+
/\/webhooks\//i,
|
|
298
|
+
/\/callback/i,
|
|
299
|
+
/\/stripe\/webhook/i,
|
|
300
|
+
/\/github\/webhook/i,
|
|
301
|
+
/\/clerk\/webhook/i,
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
// Cron/scheduled job patterns
|
|
305
|
+
const cronPatterns = [
|
|
306
|
+
/\/cron\//i,
|
|
307
|
+
/\/scheduled\//i,
|
|
308
|
+
/\/tasks?\//i,
|
|
309
|
+
/\/jobs?\//i,
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
// Check line content
|
|
313
|
+
const allPatterns = [...healthCheckPatterns, ...webhookPatterns, ...cronPatterns]
|
|
314
|
+
if (allPatterns.some(pattern => pattern.test(lineContent))) {
|
|
315
|
+
return true
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check file path
|
|
319
|
+
if (filePath.includes('/health') ||
|
|
320
|
+
filePath.includes('/webhook') ||
|
|
321
|
+
filePath.includes('/cron') ||
|
|
322
|
+
filePath.includes('/scheduled')) {
|
|
323
|
+
return true
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Check if webhook has signature verification nearby
|
|
331
|
+
*/
|
|
332
|
+
export function hasWebhookSignatureVerification(lines: string[], lineIndex: number, windowSize: number = 15): boolean {
|
|
333
|
+
const signaturePatterns = [
|
|
334
|
+
/verifySignature/i,
|
|
335
|
+
/validateSignature/i,
|
|
336
|
+
/checkSignature/i,
|
|
337
|
+
/signature.*verify/i,
|
|
338
|
+
/verify.*signature/i,
|
|
339
|
+
/hmac/i,
|
|
340
|
+
/x-hub-signature/i,
|
|
341
|
+
/stripe-signature/i,
|
|
342
|
+
/svix-signature/i,
|
|
343
|
+
/webhook.*secret/i,
|
|
344
|
+
/constructEvent/i, // Stripe webhook verification
|
|
345
|
+
/Webhook\.verify/i, // Generic webhook verify
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
const start = Math.max(0, lineIndex - windowSize)
|
|
349
|
+
const end = Math.min(lines.length, lineIndex + windowSize)
|
|
350
|
+
|
|
351
|
+
for (let i = start; i < end; i++) {
|
|
352
|
+
if (signaturePatterns.some(pattern => pattern.test(lines[i]))) {
|
|
353
|
+
return true
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return false
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Check if there's an auth check nearby (bidirectional search)
|
|
362
|
+
*/
|
|
363
|
+
export function hasAuthCheckNearby(lines: string[], lineIndex: number, windowSize: number = 20): boolean {
|
|
364
|
+
const authPatterns = [
|
|
365
|
+
/authorization/i,
|
|
366
|
+
/bearer\s+token/i,
|
|
367
|
+
/req\.user/i,
|
|
368
|
+
/request\.user/i,
|
|
369
|
+
/\.user\s*[=!]/,
|
|
370
|
+
/isAuthenticated/i,
|
|
371
|
+
/requireAuth/i,
|
|
372
|
+
/ensureAuth/i,
|
|
373
|
+
/checkAuth/i,
|
|
374
|
+
/verifyToken/i,
|
|
375
|
+
/validateToken/i,
|
|
376
|
+
/checkPermission/i,
|
|
377
|
+
/getServerSession/i,
|
|
378
|
+
/middleware.*auth/i,
|
|
379
|
+
/session\.user/i,
|
|
380
|
+
/currentUser/i,
|
|
381
|
+
/getSession\(/i,
|
|
382
|
+
/useSession\(/i,
|
|
383
|
+
/auth\(\)/i, // Next-Auth auth()
|
|
384
|
+
/withAuth/i,
|
|
385
|
+
/protected/i,
|
|
386
|
+
/verifySignature/i, // Webhook signature
|
|
387
|
+
/checkApiKey/i,
|
|
388
|
+
/validateApiKey/i,
|
|
389
|
+
/requireRole/i,
|
|
390
|
+
/hasRole/i,
|
|
391
|
+
/isAdmin/i,
|
|
392
|
+
]
|
|
393
|
+
|
|
394
|
+
// Search bidirectionally
|
|
395
|
+
const start = Math.max(0, lineIndex - windowSize)
|
|
396
|
+
const end = Math.min(lines.length, lineIndex + windowSize)
|
|
397
|
+
|
|
398
|
+
for (let i = start; i < end; i++) {
|
|
399
|
+
if (authPatterns.some(pattern => pattern.test(lines[i]))) {
|
|
400
|
+
return true
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return false
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ============================================================================
|
|
408
|
+
// BYOK (Bring Your Own Key) Context Detection
|
|
409
|
+
// ============================================================================
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Check if this appears to be a BYOK (user-provided key) context
|
|
413
|
+
* BYOK is a feature, not a vulnerability, unless improperly handled
|
|
414
|
+
*/
|
|
415
|
+
export function isBYOKContext(lineContent: string, filePath: string): boolean {
|
|
416
|
+
// Common BYOK patterns
|
|
417
|
+
const byokPatterns = [
|
|
418
|
+
/user.*api.*key/i,
|
|
419
|
+
/customer.*key/i,
|
|
420
|
+
/your.*api.*key/i,
|
|
421
|
+
/provide.*key/i,
|
|
422
|
+
/enter.*key/i,
|
|
423
|
+
/input.*key/i,
|
|
424
|
+
/form.*key/i,
|
|
425
|
+
/settings.*key/i,
|
|
426
|
+
/config.*key.*user/i,
|
|
427
|
+
/BYOK/i,
|
|
428
|
+
/bring.*your.*own/i,
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
// Form/input contexts
|
|
432
|
+
const inputPatterns = [
|
|
433
|
+
/input.*type/i,
|
|
434
|
+
/onChange/i,
|
|
435
|
+
/onSubmit/i,
|
|
436
|
+
/handleSubmit/i,
|
|
437
|
+
/useState.*key/i,
|
|
438
|
+
/form.*data/i,
|
|
439
|
+
]
|
|
440
|
+
|
|
441
|
+
// Settings/config UI patterns
|
|
442
|
+
const settingsPatterns = [
|
|
443
|
+
/\/settings\//i,
|
|
444
|
+
/\/config\//i,
|
|
445
|
+
/\/preferences\//i,
|
|
446
|
+
/\/profile\//i,
|
|
447
|
+
]
|
|
448
|
+
|
|
449
|
+
// Check line content
|
|
450
|
+
if (byokPatterns.some(p => p.test(lineContent)) ||
|
|
451
|
+
inputPatterns.some(p => p.test(lineContent))) {
|
|
452
|
+
return true
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Check file path
|
|
456
|
+
if (settingsPatterns.some(p => p.test(filePath))) {
|
|
457
|
+
// In settings files, look for user input context
|
|
458
|
+
if (inputPatterns.some(p => p.test(lineContent))) {
|
|
459
|
+
return true
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return false
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Check if key is being stored/handled properly (not exposed)
|
|
468
|
+
*/
|
|
469
|
+
export function isKeyProperlyHandled(lineContent: string, lines: string[], lineIndex: number): boolean {
|
|
470
|
+
// Proper handling patterns (encryption, secure storage, etc.)
|
|
471
|
+
const properHandlingPatterns = [
|
|
472
|
+
/encrypt/i,
|
|
473
|
+
/hash/i,
|
|
474
|
+
/secure.*storage/i,
|
|
475
|
+
/keychain/i,
|
|
476
|
+
/vault/i,
|
|
477
|
+
/secretsManager/i,
|
|
478
|
+
/kms/i,
|
|
479
|
+
/\.env/i,
|
|
480
|
+
]
|
|
481
|
+
|
|
482
|
+
// Check current line
|
|
483
|
+
if (properHandlingPatterns.some(p => p.test(lineContent))) {
|
|
484
|
+
return true
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Check nearby lines (5 lines before and after)
|
|
488
|
+
const start = Math.max(0, lineIndex - 5)
|
|
489
|
+
const end = Math.min(lines.length, lineIndex + 5)
|
|
490
|
+
|
|
491
|
+
for (let i = start; i < end; i++) {
|
|
492
|
+
if (properHandlingPatterns.some(p => p.test(lines[i]))) {
|
|
493
|
+
return true
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return false
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ============================================================================
|
|
501
|
+
// Service Role Key Context
|
|
502
|
+
// ============================================================================
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Check if this is a service role key usage that's acceptable
|
|
506
|
+
* Server-only + env var = acceptable
|
|
507
|
+
* Client exposure = critical
|
|
508
|
+
*/
|
|
509
|
+
export function getServiceRoleKeyContext(
|
|
510
|
+
lineContent: string,
|
|
511
|
+
filePath: string
|
|
512
|
+
): 'safe_server' | 'needs_review' | 'client_exposure' {
|
|
513
|
+
const isServer = isServerOnlyFile(filePath)
|
|
514
|
+
const usesEnvVar = isEnvVarReference(lineContent)
|
|
515
|
+
const isClientFile = isClientBundledFile(filePath)
|
|
516
|
+
const isNextPublic = isNextPublicEnvVar(lineContent)
|
|
517
|
+
|
|
518
|
+
// NEXT_PUBLIC_ service role key = always critical (client exposure)
|
|
519
|
+
if (isNextPublic) {
|
|
520
|
+
return 'client_exposure'
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Server-only file using env var = safe
|
|
524
|
+
if (isServer && usesEnvVar) {
|
|
525
|
+
return 'safe_server'
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Client-bundled file = exposure risk
|
|
529
|
+
if (isClientFile) {
|
|
530
|
+
return 'client_exposure'
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Hardcoded or ambiguous = needs review
|
|
534
|
+
return 'needs_review'
|
|
535
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Detection Utility
|
|
3
|
+
* Compares current repository tree against previous scan data to detect changed files
|
|
4
|
+
* Used for incremental scanning (Story D)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface TreeFileInfo {
|
|
8
|
+
path: string
|
|
9
|
+
sha: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DiffResult {
|
|
13
|
+
/** Files that are new (not in previous scan) */
|
|
14
|
+
added: string[]
|
|
15
|
+
/** Files whose SHA changed (content modified) */
|
|
16
|
+
modified: string[]
|
|
17
|
+
/** Files that were deleted (in previous scan but not in current) */
|
|
18
|
+
deleted: string[]
|
|
19
|
+
/** Files that haven't changed */
|
|
20
|
+
unchanged: string[]
|
|
21
|
+
/** Total count of changed files (added + modified) */
|
|
22
|
+
changedCount: number
|
|
23
|
+
/** Whether this is a significant change warranting incremental scan */
|
|
24
|
+
shouldUseIncremental: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maximum number of changed files before falling back to full scan
|
|
29
|
+
* Beyond this threshold, incremental mode provides diminishing returns
|
|
30
|
+
*/
|
|
31
|
+
const MAX_CHANGED_FILES_FOR_INCREMENTAL = 50
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Detect which files have changed between the current tree and previous scan
|
|
35
|
+
*
|
|
36
|
+
* @param currentTree - Array of files from current GitHub tree (path + sha)
|
|
37
|
+
* @param previousFileShas - Map of {file_path: sha} from previous scan
|
|
38
|
+
* @returns DiffResult with categorized file changes
|
|
39
|
+
*/
|
|
40
|
+
export function detectChangedFiles(
|
|
41
|
+
currentTree: TreeFileInfo[],
|
|
42
|
+
previousFileShas: Record<string, string> | null | undefined
|
|
43
|
+
): DiffResult {
|
|
44
|
+
// If no previous data, everything is "new"
|
|
45
|
+
if (!previousFileShas || Object.keys(previousFileShas).length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
added: currentTree.map(f => f.path),
|
|
48
|
+
modified: [],
|
|
49
|
+
deleted: [],
|
|
50
|
+
unchanged: [],
|
|
51
|
+
changedCount: currentTree.length,
|
|
52
|
+
shouldUseIncremental: false, // No baseline = full scan
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const added: string[] = []
|
|
57
|
+
const modified: string[] = []
|
|
58
|
+
const unchanged: string[] = []
|
|
59
|
+
const currentPaths = new Set<string>()
|
|
60
|
+
|
|
61
|
+
// Compare current files against previous
|
|
62
|
+
for (const file of currentTree) {
|
|
63
|
+
currentPaths.add(file.path)
|
|
64
|
+
const previousSha = previousFileShas[file.path]
|
|
65
|
+
|
|
66
|
+
if (!previousSha) {
|
|
67
|
+
// File didn't exist before
|
|
68
|
+
added.push(file.path)
|
|
69
|
+
} else if (previousSha !== file.sha) {
|
|
70
|
+
// File content changed (SHA differs)
|
|
71
|
+
modified.push(file.path)
|
|
72
|
+
} else {
|
|
73
|
+
// Same SHA = unchanged
|
|
74
|
+
unchanged.push(file.path)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Find deleted files (in previous but not in current)
|
|
79
|
+
const deleted: string[] = []
|
|
80
|
+
for (const path of Object.keys(previousFileShas)) {
|
|
81
|
+
if (!currentPaths.has(path)) {
|
|
82
|
+
deleted.push(path)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const changedCount = added.length + modified.length
|
|
87
|
+
const shouldUseIncremental = changedCount > 0 && changedCount <= MAX_CHANGED_FILES_FOR_INCREMENTAL
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
added,
|
|
91
|
+
modified,
|
|
92
|
+
deleted,
|
|
93
|
+
unchanged,
|
|
94
|
+
changedCount,
|
|
95
|
+
shouldUseIncremental,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build a file SHA map from a GitHub tree response
|
|
101
|
+
* Filters to only include blob (file) entries
|
|
102
|
+
*
|
|
103
|
+
* @param tree - GitHub tree items with path and sha
|
|
104
|
+
* @returns Map of {file_path: sha}
|
|
105
|
+
*/
|
|
106
|
+
export function buildFileShaMap(
|
|
107
|
+
tree: Array<{ path: string; sha: string; type: 'blob' | 'tree' }>
|
|
108
|
+
): Record<string, string> {
|
|
109
|
+
const map: Record<string, string> = {}
|
|
110
|
+
|
|
111
|
+
for (const item of tree) {
|
|
112
|
+
// Only include files (blobs), not directories (trees)
|
|
113
|
+
if (item.type === 'blob') {
|
|
114
|
+
map[item.path] = item.sha
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return map
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Check if two tree SHAs are different
|
|
123
|
+
* Quick check before doing detailed file comparison
|
|
124
|
+
*
|
|
125
|
+
* @param currentTreeSha - SHA of current tree
|
|
126
|
+
* @param previousTreeSha - SHA from previous scan
|
|
127
|
+
* @returns true if trees are different (need detailed comparison)
|
|
128
|
+
*/
|
|
129
|
+
export function hasTreeChanged(
|
|
130
|
+
currentTreeSha: string,
|
|
131
|
+
previousTreeSha: string | null | undefined
|
|
132
|
+
): boolean {
|
|
133
|
+
if (!previousTreeSha) return true
|
|
134
|
+
return currentTreeSha !== previousTreeSha
|
|
135
|
+
}
|