@oculum/scanner 1.0.9 → 1.0.11
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/baseline/diff.d.ts +32 -0
- package/dist/baseline/diff.d.ts.map +1 -0
- package/dist/baseline/diff.js +119 -0
- package/dist/baseline/diff.js.map +1 -0
- package/dist/baseline/index.d.ts +9 -0
- package/dist/baseline/index.d.ts.map +1 -0
- package/dist/baseline/index.js +19 -0
- package/dist/baseline/index.js.map +1 -0
- package/dist/baseline/manager.d.ts +67 -0
- package/dist/baseline/manager.d.ts.map +1 -0
- package/dist/baseline/manager.js +180 -0
- package/dist/baseline/manager.js.map +1 -0
- package/dist/baseline/types.d.ts +91 -0
- package/dist/baseline/types.d.ts.map +1 -0
- package/dist/baseline/types.js +12 -0
- package/dist/baseline/types.js.map +1 -0
- package/dist/formatters/cli-terminal.d.ts +38 -0
- package/dist/formatters/cli-terminal.d.ts.map +1 -1
- package/dist/formatters/cli-terminal.js +365 -42
- package/dist/formatters/cli-terminal.js.map +1 -1
- package/dist/formatters/github-comment.d.ts +1 -1
- package/dist/formatters/github-comment.d.ts.map +1 -1
- package/dist/formatters/github-comment.js +75 -11
- package/dist/formatters/github-comment.js.map +1 -1
- package/dist/formatters/index.d.ts +1 -1
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +4 -1
- package/dist/formatters/index.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +155 -16
- package/dist/index.js.map +1 -1
- package/dist/layer1/config-audit.d.ts.map +1 -1
- package/dist/layer1/config-audit.js +20 -3
- package/dist/layer1/config-audit.js.map +1 -1
- package/dist/layer1/config-mcp-audit.d.ts +20 -0
- package/dist/layer1/config-mcp-audit.d.ts.map +1 -0
- package/dist/layer1/config-mcp-audit.js +239 -0
- package/dist/layer1/config-mcp-audit.js.map +1 -0
- package/dist/layer1/index.d.ts +1 -0
- package/dist/layer1/index.d.ts.map +1 -1
- package/dist/layer1/index.js +9 -1
- package/dist/layer1/index.js.map +1 -1
- package/dist/layer2/ai-agent-tools.d.ts.map +1 -1
- package/dist/layer2/ai-agent-tools.js +303 -0
- package/dist/layer2/ai-agent-tools.js.map +1 -1
- package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -1
- package/dist/layer2/ai-endpoint-protection.js +17 -3
- package/dist/layer2/ai-endpoint-protection.js.map +1 -1
- package/dist/layer2/ai-execution-sinks.d.ts.map +1 -1
- package/dist/layer2/ai-execution-sinks.js +462 -12
- package/dist/layer2/ai-execution-sinks.js.map +1 -1
- package/dist/layer2/ai-fingerprinting.d.ts.map +1 -1
- package/dist/layer2/ai-fingerprinting.js +3 -0
- package/dist/layer2/ai-fingerprinting.js.map +1 -1
- package/dist/layer2/ai-mcp-security.d.ts +17 -0
- package/dist/layer2/ai-mcp-security.d.ts.map +1 -0
- package/dist/layer2/ai-mcp-security.js +679 -0
- package/dist/layer2/ai-mcp-security.js.map +1 -0
- package/dist/layer2/ai-package-hallucination.d.ts +19 -0
- package/dist/layer2/ai-package-hallucination.d.ts.map +1 -0
- package/dist/layer2/ai-package-hallucination.js +696 -0
- package/dist/layer2/ai-package-hallucination.js.map +1 -0
- package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -1
- package/dist/layer2/ai-prompt-hygiene.js +495 -9
- package/dist/layer2/ai-prompt-hygiene.js.map +1 -1
- package/dist/layer2/ai-rag-safety.d.ts.map +1 -1
- package/dist/layer2/ai-rag-safety.js +372 -1
- package/dist/layer2/ai-rag-safety.js.map +1 -1
- package/dist/layer2/auth-antipatterns.d.ts.map +1 -1
- package/dist/layer2/auth-antipatterns.js +4 -0
- package/dist/layer2/auth-antipatterns.js.map +1 -1
- package/dist/layer2/byok-patterns.d.ts.map +1 -1
- package/dist/layer2/byok-patterns.js +3 -0
- package/dist/layer2/byok-patterns.js.map +1 -1
- package/dist/layer2/dangerous-functions/child-process.d.ts +16 -0
- package/dist/layer2/dangerous-functions/child-process.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/child-process.js +74 -0
- package/dist/layer2/dangerous-functions/child-process.js.map +1 -0
- package/dist/layer2/dangerous-functions/dom-xss.d.ts +29 -0
- package/dist/layer2/dangerous-functions/dom-xss.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/dom-xss.js +179 -0
- package/dist/layer2/dangerous-functions/dom-xss.js.map +1 -0
- package/dist/layer2/dangerous-functions/index.d.ts +13 -0
- package/dist/layer2/dangerous-functions/index.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/index.js +621 -0
- package/dist/layer2/dangerous-functions/index.js.map +1 -0
- package/dist/layer2/dangerous-functions/json-parse.d.ts +31 -0
- package/dist/layer2/dangerous-functions/json-parse.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/json-parse.js +319 -0
- package/dist/layer2/dangerous-functions/json-parse.js.map +1 -0
- package/dist/layer2/dangerous-functions/math-random.d.ts +61 -0
- package/dist/layer2/dangerous-functions/math-random.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/math-random.js +459 -0
- package/dist/layer2/dangerous-functions/math-random.js.map +1 -0
- package/dist/layer2/dangerous-functions/patterns.d.ts +21 -0
- package/dist/layer2/dangerous-functions/patterns.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/patterns.js +161 -0
- package/dist/layer2/dangerous-functions/patterns.js.map +1 -0
- package/dist/layer2/dangerous-functions/request-validation.d.ts +13 -0
- package/dist/layer2/dangerous-functions/request-validation.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/request-validation.js +119 -0
- package/dist/layer2/dangerous-functions/request-validation.js.map +1 -0
- package/dist/layer2/dangerous-functions/utils/control-flow.d.ts +23 -0
- package/dist/layer2/dangerous-functions/utils/control-flow.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/utils/control-flow.js +149 -0
- package/dist/layer2/dangerous-functions/utils/control-flow.js.map +1 -0
- package/dist/layer2/dangerous-functions/utils/helpers.d.ts +31 -0
- package/dist/layer2/dangerous-functions/utils/helpers.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/utils/helpers.js +124 -0
- package/dist/layer2/dangerous-functions/utils/helpers.js.map +1 -0
- package/dist/layer2/dangerous-functions/utils/index.d.ts +9 -0
- package/dist/layer2/dangerous-functions/utils/index.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/utils/index.js +23 -0
- package/dist/layer2/dangerous-functions/utils/index.js.map +1 -0
- package/dist/layer2/dangerous-functions/utils/schema-validation.d.ts +22 -0
- package/dist/layer2/dangerous-functions/utils/schema-validation.d.ts.map +1 -0
- package/dist/layer2/dangerous-functions/utils/schema-validation.js +89 -0
- package/dist/layer2/dangerous-functions/utils/schema-validation.js.map +1 -0
- package/dist/layer2/data-exposure.d.ts.map +1 -1
- package/dist/layer2/data-exposure.js +3 -0
- package/dist/layer2/data-exposure.js.map +1 -1
- package/dist/layer2/framework-checks.d.ts.map +1 -1
- package/dist/layer2/framework-checks.js +3 -0
- package/dist/layer2/framework-checks.js.map +1 -1
- package/dist/layer2/index.d.ts +3 -0
- package/dist/layer2/index.d.ts.map +1 -1
- package/dist/layer2/index.js +61 -2
- package/dist/layer2/index.js.map +1 -1
- package/dist/layer2/logic-gates.d.ts.map +1 -1
- package/dist/layer2/logic-gates.js +4 -0
- package/dist/layer2/logic-gates.js.map +1 -1
- package/dist/layer2/model-supply-chain.d.ts +20 -0
- package/dist/layer2/model-supply-chain.d.ts.map +1 -0
- package/dist/layer2/model-supply-chain.js +376 -0
- package/dist/layer2/model-supply-chain.js.map +1 -0
- package/dist/layer2/risky-imports.d.ts.map +1 -1
- package/dist/layer2/risky-imports.js +4 -0
- package/dist/layer2/risky-imports.js.map +1 -1
- package/dist/layer2/variables.d.ts.map +1 -1
- package/dist/layer2/variables.js +4 -0
- package/dist/layer2/variables.js.map +1 -1
- package/dist/layer3/anthropic/auto-dismiss.d.ts +24 -0
- package/dist/layer3/anthropic/auto-dismiss.d.ts.map +1 -0
- package/dist/layer3/anthropic/auto-dismiss.js +188 -0
- package/dist/layer3/anthropic/auto-dismiss.js.map +1 -0
- package/dist/layer3/anthropic/clients.d.ts +44 -0
- package/dist/layer3/anthropic/clients.d.ts.map +1 -0
- package/dist/layer3/anthropic/clients.js +81 -0
- package/dist/layer3/anthropic/clients.js.map +1 -0
- package/dist/layer3/anthropic/index.d.ts +41 -0
- package/dist/layer3/anthropic/index.d.ts.map +1 -0
- package/dist/layer3/anthropic/index.js +141 -0
- package/dist/layer3/anthropic/index.js.map +1 -0
- package/dist/layer3/anthropic/prompts/index.d.ts +8 -0
- package/dist/layer3/anthropic/prompts/index.d.ts.map +1 -0
- package/dist/layer3/anthropic/prompts/index.js +14 -0
- package/dist/layer3/anthropic/prompts/index.js.map +1 -0
- package/dist/layer3/anthropic/prompts/semantic-analysis.d.ts +15 -0
- package/dist/layer3/anthropic/prompts/semantic-analysis.d.ts.map +1 -0
- package/dist/layer3/anthropic/prompts/semantic-analysis.js +169 -0
- package/dist/layer3/anthropic/prompts/semantic-analysis.js.map +1 -0
- package/dist/layer3/anthropic/prompts/validation.d.ts +12 -0
- package/dist/layer3/anthropic/prompts/validation.d.ts.map +1 -0
- package/dist/layer3/anthropic/prompts/validation.js +421 -0
- package/dist/layer3/anthropic/prompts/validation.js.map +1 -0
- package/dist/layer3/anthropic/providers/anthropic.d.ts +21 -0
- package/dist/layer3/anthropic/providers/anthropic.d.ts.map +1 -0
- package/dist/layer3/anthropic/providers/anthropic.js +266 -0
- package/dist/layer3/anthropic/providers/anthropic.js.map +1 -0
- package/dist/layer3/anthropic/providers/index.d.ts +8 -0
- package/dist/layer3/anthropic/providers/index.d.ts.map +1 -0
- package/dist/layer3/anthropic/providers/index.js +15 -0
- package/dist/layer3/anthropic/providers/index.js.map +1 -0
- package/dist/layer3/anthropic/providers/openai.d.ts +18 -0
- package/dist/layer3/anthropic/providers/openai.d.ts.map +1 -0
- package/dist/layer3/anthropic/providers/openai.js +340 -0
- package/dist/layer3/anthropic/providers/openai.js.map +1 -0
- package/dist/layer3/anthropic/request-builder.d.ts +20 -0
- package/dist/layer3/anthropic/request-builder.d.ts.map +1 -0
- package/dist/layer3/anthropic/request-builder.js +134 -0
- package/dist/layer3/anthropic/request-builder.js.map +1 -0
- package/dist/layer3/anthropic/types.d.ts +88 -0
- package/dist/layer3/anthropic/types.d.ts.map +1 -0
- package/dist/layer3/anthropic/types.js +38 -0
- package/dist/layer3/anthropic/types.js.map +1 -0
- package/dist/layer3/anthropic/utils/index.d.ts +9 -0
- package/dist/layer3/anthropic/utils/index.d.ts.map +1 -0
- package/dist/layer3/anthropic/utils/index.js +24 -0
- package/dist/layer3/anthropic/utils/index.js.map +1 -0
- package/dist/layer3/anthropic/utils/path-helpers.d.ts +21 -0
- package/dist/layer3/anthropic/utils/path-helpers.d.ts.map +1 -0
- package/dist/layer3/anthropic/utils/path-helpers.js +69 -0
- package/dist/layer3/anthropic/utils/path-helpers.js.map +1 -0
- package/dist/layer3/anthropic/utils/response-parser.d.ts +40 -0
- package/dist/layer3/anthropic/utils/response-parser.d.ts.map +1 -0
- package/dist/layer3/anthropic/utils/response-parser.js +285 -0
- package/dist/layer3/anthropic/utils/response-parser.js.map +1 -0
- package/dist/layer3/anthropic/utils/retry.d.ts +15 -0
- package/dist/layer3/anthropic/utils/retry.d.ts.map +1 -0
- package/dist/layer3/anthropic/utils/retry.js +62 -0
- package/dist/layer3/anthropic/utils/retry.js.map +1 -0
- package/dist/layer3/index.d.ts +1 -0
- package/dist/layer3/index.d.ts.map +1 -1
- package/dist/layer3/index.js +16 -6
- package/dist/layer3/index.js.map +1 -1
- package/dist/layer3/osv-check.d.ts +75 -0
- package/dist/layer3/osv-check.d.ts.map +1 -0
- package/dist/layer3/osv-check.js +308 -0
- package/dist/layer3/osv-check.js.map +1 -0
- package/dist/rules/framework-fixes.d.ts +48 -0
- package/dist/rules/framework-fixes.d.ts.map +1 -0
- package/dist/rules/framework-fixes.js +439 -0
- package/dist/rules/framework-fixes.js.map +1 -0
- package/dist/rules/index.d.ts +8 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +18 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/metadata.d.ts +43 -0
- package/dist/rules/metadata.d.ts.map +1 -0
- package/dist/rules/metadata.js +734 -0
- package/dist/rules/metadata.js.map +1 -0
- package/dist/suppression/config-loader.d.ts +74 -0
- package/dist/suppression/config-loader.d.ts.map +1 -0
- package/dist/suppression/config-loader.js +424 -0
- package/dist/suppression/config-loader.js.map +1 -0
- package/dist/suppression/hash.d.ts +48 -0
- package/dist/suppression/hash.d.ts.map +1 -0
- package/dist/suppression/hash.js +88 -0
- package/dist/suppression/hash.js.map +1 -0
- package/dist/suppression/index.d.ts +11 -0
- package/dist/suppression/index.d.ts.map +1 -0
- package/dist/suppression/index.js +39 -0
- package/dist/suppression/index.js.map +1 -0
- package/dist/suppression/inline-parser.d.ts +39 -0
- package/dist/suppression/inline-parser.d.ts.map +1 -0
- package/dist/suppression/inline-parser.js +218 -0
- package/dist/suppression/inline-parser.js.map +1 -0
- package/dist/suppression/manager.d.ts +94 -0
- package/dist/suppression/manager.d.ts.map +1 -0
- package/dist/suppression/manager.js +292 -0
- package/dist/suppression/manager.js.map +1 -0
- package/dist/suppression/types.d.ts +151 -0
- package/dist/suppression/types.d.ts.map +1 -0
- package/dist/suppression/types.js +28 -0
- package/dist/suppression/types.js.map +1 -0
- package/dist/tiers.d.ts +1 -1
- package/dist/tiers.d.ts.map +1 -1
- package/dist/tiers.js +27 -0
- package/dist/tiers.js.map +1 -1
- package/dist/types.d.ts +62 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/context-helpers.d.ts +4 -0
- package/dist/utils/context-helpers.d.ts.map +1 -1
- package/dist/utils/context-helpers.js +13 -9
- package/dist/utils/context-helpers.js.map +1 -1
- package/package.json +4 -2
- package/src/__tests__/benchmark/fixtures/layer1/mcp-config-audit.json +31 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-execution-sinks.ts +1489 -82
- package/src/__tests__/benchmark/fixtures/layer2/ai-mcp-security.ts +495 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-package-hallucination.ts +255 -0
- package/src/__tests__/benchmark/fixtures/layer2/ai-prompt-hygiene.ts +300 -1
- package/src/__tests__/benchmark/fixtures/layer2/ai-rag-safety.ts +139 -0
- package/src/__tests__/benchmark/fixtures/layer2/byok-patterns.ts +7 -0
- package/src/__tests__/benchmark/fixtures/layer2/data-exposure.ts +63 -0
- package/src/__tests__/benchmark/fixtures/layer2/excessive-agency.ts +221 -0
- package/src/__tests__/benchmark/fixtures/layer2/index.ts +18 -0
- package/src/__tests__/benchmark/fixtures/layer2/model-supply-chain.ts +204 -0
- package/src/__tests__/benchmark/fixtures/layer2/phase1-enhancements.ts +157 -0
- package/src/__tests__/snapshots/__snapshots__/anthropic-validation-refactor.test.ts.snap +758 -0
- package/src/__tests__/snapshots/__snapshots__/dangerous-functions-refactor.test.ts.snap +503 -0
- package/src/__tests__/snapshots/anthropic-validation-refactor.test.ts +321 -0
- package/src/__tests__/snapshots/dangerous-functions-refactor.test.ts +439 -0
- package/src/baseline/__tests__/diff.test.ts +261 -0
- package/src/baseline/__tests__/manager.test.ts +225 -0
- package/src/baseline/diff.ts +135 -0
- package/src/baseline/index.ts +29 -0
- package/src/baseline/manager.ts +230 -0
- package/src/baseline/types.ts +97 -0
- package/src/formatters/cli-terminal.ts +444 -41
- package/src/formatters/github-comment.ts +79 -11
- package/src/formatters/index.ts +4 -0
- package/src/index.ts +197 -14
- package/src/layer1/config-audit.ts +24 -3
- package/src/layer1/config-mcp-audit.ts +276 -0
- package/src/layer1/index.ts +16 -6
- package/src/layer2/ai-agent-tools.ts +336 -0
- package/src/layer2/ai-endpoint-protection.ts +16 -3
- package/src/layer2/ai-execution-sinks.ts +516 -12
- package/src/layer2/ai-fingerprinting.ts +5 -1
- package/src/layer2/ai-mcp-security.ts +730 -0
- package/src/layer2/ai-package-hallucination.ts +791 -0
- package/src/layer2/ai-prompt-hygiene.ts +547 -9
- package/src/layer2/ai-rag-safety.ts +382 -3
- package/src/layer2/auth-antipatterns.ts +5 -0
- package/src/layer2/byok-patterns.ts +5 -1
- package/src/layer2/dangerous-functions/child-process.ts +98 -0
- package/src/layer2/dangerous-functions/dom-xss.ts +220 -0
- package/src/layer2/dangerous-functions/index.ts +949 -0
- package/src/layer2/dangerous-functions/json-parse.ts +385 -0
- package/src/layer2/dangerous-functions/math-random.ts +537 -0
- package/src/layer2/dangerous-functions/patterns.ts +174 -0
- package/src/layer2/dangerous-functions/request-validation.ts +145 -0
- package/src/layer2/dangerous-functions/utils/control-flow.ts +162 -0
- package/src/layer2/dangerous-functions/utils/helpers.ts +170 -0
- package/src/layer2/dangerous-functions/utils/index.ts +25 -0
- package/src/layer2/dangerous-functions/utils/schema-validation.ts +91 -0
- package/src/layer2/data-exposure.ts +5 -1
- package/src/layer2/framework-checks.ts +5 -0
- package/src/layer2/index.ts +63 -1
- package/src/layer2/logic-gates.ts +5 -0
- package/src/layer2/model-supply-chain.ts +456 -0
- package/src/layer2/risky-imports.ts +5 -0
- package/src/layer2/variables.ts +5 -0
- package/src/layer3/__tests__/osv-check.test.ts +384 -0
- package/src/layer3/anthropic/auto-dismiss.ts +212 -0
- package/src/layer3/anthropic/clients.ts +84 -0
- package/src/layer3/anthropic/index.ts +170 -0
- package/src/layer3/anthropic/prompts/index.ts +14 -0
- package/src/layer3/anthropic/prompts/semantic-analysis.ts +173 -0
- package/src/layer3/anthropic/prompts/validation.ts +419 -0
- package/src/layer3/anthropic/providers/anthropic.ts +310 -0
- package/src/layer3/anthropic/providers/index.ts +8 -0
- package/src/layer3/anthropic/providers/openai.ts +384 -0
- package/src/layer3/anthropic/request-builder.ts +150 -0
- package/src/layer3/anthropic/types.ts +148 -0
- package/src/layer3/anthropic/utils/index.ts +26 -0
- package/src/layer3/anthropic/utils/path-helpers.ts +68 -0
- package/src/layer3/anthropic/utils/response-parser.ts +322 -0
- package/src/layer3/anthropic/utils/retry.ts +75 -0
- package/src/layer3/index.ts +18 -5
- package/src/layer3/osv-check.ts +420 -0
- package/src/rules/__tests__/framework-fixes.test.ts +689 -0
- package/src/rules/__tests__/metadata.test.ts +218 -0
- package/src/rules/framework-fixes.ts +470 -0
- package/src/rules/index.ts +21 -0
- package/src/rules/metadata.ts +831 -0
- package/src/suppression/__tests__/config-loader.test.ts +382 -0
- package/src/suppression/__tests__/hash.test.ts +166 -0
- package/src/suppression/__tests__/inline-parser.test.ts +212 -0
- package/src/suppression/__tests__/manager.test.ts +415 -0
- package/src/suppression/config-loader.ts +462 -0
- package/src/suppression/hash.ts +95 -0
- package/src/suppression/index.ts +51 -0
- package/src/suppression/inline-parser.ts +273 -0
- package/src/suppression/manager.ts +379 -0
- package/src/suppression/types.ts +174 -0
- package/src/tiers.ts +36 -0
- package/src/types.ts +90 -0
- package/src/utils/context-helpers.ts +13 -9
- package/dist/layer2/dangerous-functions.d.ts +0 -7
- package/dist/layer2/dangerous-functions.d.ts.map +0 -1
- package/dist/layer2/dangerous-functions.js +0 -1701
- package/dist/layer2/dangerous-functions.js.map +0 -1
- package/dist/layer3/anthropic.d.ts +0 -87
- package/dist/layer3/anthropic.d.ts.map +0 -1
- package/dist/layer3/anthropic.js +0 -1948
- package/dist/layer3/anthropic.js.map +0 -1
- package/dist/layer3/openai.d.ts +0 -25
- package/dist/layer3/openai.d.ts.map +0 -1
- package/dist/layer3/openai.js +0 -238
- package/dist/layer3/openai.js.map +0 -1
- package/src/layer2/dangerous-functions.ts +0 -1940
- package/src/layer3/anthropic.ts +0 -2257
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline Manager Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { tmpdir } from 'os'
|
|
8
|
+
import { BaselineManager } from '../manager'
|
|
9
|
+
import type { ScanResult } from '../../types'
|
|
10
|
+
import type { BaselineData } from '../types'
|
|
11
|
+
|
|
12
|
+
describe('BaselineManager', () => {
|
|
13
|
+
let testDir: string
|
|
14
|
+
let manager: BaselineManager
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Create a unique temp directory for each test
|
|
18
|
+
testDir = join(tmpdir(), `baseline-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
|
|
19
|
+
mkdirSync(testDir, { recursive: true })
|
|
20
|
+
manager = new BaselineManager({ projectPath: testDir })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
// Clean up test directory
|
|
25
|
+
try {
|
|
26
|
+
rmSync(testDir, { recursive: true, force: true })
|
|
27
|
+
} catch {
|
|
28
|
+
// Ignore cleanup errors
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('loadBaseline', () => {
|
|
33
|
+
it('should return found: false when no baseline exists', () => {
|
|
34
|
+
const result = manager.loadBaseline()
|
|
35
|
+
expect(result.found).toBe(false)
|
|
36
|
+
expect(result.baseline).toBeUndefined()
|
|
37
|
+
expect(result.error).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should load a valid baseline', () => {
|
|
41
|
+
// Create a valid baseline file
|
|
42
|
+
const baseline: BaselineData = {
|
|
43
|
+
version: 1,
|
|
44
|
+
createdAt: '2024-01-15T10:00:00.000Z',
|
|
45
|
+
commit: 'abc1234',
|
|
46
|
+
branch: 'main',
|
|
47
|
+
findings: [
|
|
48
|
+
{
|
|
49
|
+
hash: 'abcd1234abcd1234',
|
|
50
|
+
filePath: 'src/test.ts',
|
|
51
|
+
lineNumber: 10,
|
|
52
|
+
category: 'hardcoded_secret',
|
|
53
|
+
severity: 'critical',
|
|
54
|
+
title: 'Hardcoded API key',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
stats: {
|
|
58
|
+
total: 1,
|
|
59
|
+
critical: 1,
|
|
60
|
+
high: 0,
|
|
61
|
+
medium: 0,
|
|
62
|
+
low: 0,
|
|
63
|
+
info: 0,
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
mkdirSync(join(testDir, '.oculum'), { recursive: true })
|
|
68
|
+
writeFileSync(join(testDir, '.oculum/baseline.json'), JSON.stringify(baseline))
|
|
69
|
+
|
|
70
|
+
const result = manager.loadBaseline()
|
|
71
|
+
expect(result.found).toBe(true)
|
|
72
|
+
expect(result.baseline).toEqual(baseline)
|
|
73
|
+
expect(result.error).toBeUndefined()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should return error for invalid JSON', () => {
|
|
77
|
+
mkdirSync(join(testDir, '.oculum'), { recursive: true })
|
|
78
|
+
writeFileSync(join(testDir, '.oculum/baseline.json'), 'not valid json')
|
|
79
|
+
|
|
80
|
+
const result = manager.loadBaseline()
|
|
81
|
+
expect(result.found).toBe(false)
|
|
82
|
+
expect(result.error).toContain('Failed to parse baseline')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('should return error for unsupported version', () => {
|
|
86
|
+
const baseline = {
|
|
87
|
+
version: 99,
|
|
88
|
+
createdAt: '2024-01-15T10:00:00.000Z',
|
|
89
|
+
findings: [],
|
|
90
|
+
stats: { total: 0, critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
mkdirSync(join(testDir, '.oculum'), { recursive: true })
|
|
94
|
+
writeFileSync(join(testDir, '.oculum/baseline.json'), JSON.stringify(baseline))
|
|
95
|
+
|
|
96
|
+
const result = manager.loadBaseline()
|
|
97
|
+
expect(result.found).toBe(false)
|
|
98
|
+
expect(result.error).toContain('Unsupported baseline version')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should return error when findings array is missing', () => {
|
|
102
|
+
const baseline = {
|
|
103
|
+
version: 1,
|
|
104
|
+
createdAt: '2024-01-15T10:00:00.000Z',
|
|
105
|
+
stats: { total: 0, critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
mkdirSync(join(testDir, '.oculum'), { recursive: true })
|
|
109
|
+
writeFileSync(join(testDir, '.oculum/baseline.json'), JSON.stringify(baseline))
|
|
110
|
+
|
|
111
|
+
const result = manager.loadBaseline()
|
|
112
|
+
expect(result.found).toBe(false)
|
|
113
|
+
expect(result.error).toContain('missing findings array')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('saveBaseline', () => {
|
|
118
|
+
it('should save a baseline from scan result', () => {
|
|
119
|
+
const scanResult: ScanResult = {
|
|
120
|
+
repoName: 'test/repo',
|
|
121
|
+
repoUrl: 'https://github.com/test/repo',
|
|
122
|
+
branch: 'main',
|
|
123
|
+
filesScanned: 10,
|
|
124
|
+
filesSkipped: 2,
|
|
125
|
+
vulnerabilities: [
|
|
126
|
+
{
|
|
127
|
+
filePath: 'src/test.ts',
|
|
128
|
+
lineNumber: 10,
|
|
129
|
+
category: 'hardcoded_secret',
|
|
130
|
+
severity: 'critical',
|
|
131
|
+
title: 'Hardcoded API key',
|
|
132
|
+
description: 'Test description',
|
|
133
|
+
confidence: 'high',
|
|
134
|
+
layer: 1,
|
|
135
|
+
lineContent: 'const API_KEY = "sk-1234567890"',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
severityCounts: { critical: 1, high: 0, medium: 0, low: 0, info: 0 },
|
|
139
|
+
categoryCounts: {},
|
|
140
|
+
hasBlockingIssues: true,
|
|
141
|
+
scanDuration: 1000,
|
|
142
|
+
timestamp: '2024-01-15T10:00:00.000Z',
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = manager.saveBaseline(scanResult, { scanDepth: 'cheap' })
|
|
146
|
+
|
|
147
|
+
expect(result.success).toBe(true)
|
|
148
|
+
expect(existsSync(result.path)).toBe(true)
|
|
149
|
+
|
|
150
|
+
// Verify saved content
|
|
151
|
+
const content = readFileSync(result.path, 'utf-8')
|
|
152
|
+
const baseline = JSON.parse(content) as BaselineData
|
|
153
|
+
|
|
154
|
+
expect(baseline.version).toBe(1)
|
|
155
|
+
expect(baseline.findings).toHaveLength(1)
|
|
156
|
+
expect(baseline.findings[0].title).toBe('Hardcoded API key')
|
|
157
|
+
expect(baseline.stats.critical).toBe(1)
|
|
158
|
+
expect(baseline.scanDepth).toBe('cheap')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should create .oculum directory if it does not exist', () => {
|
|
162
|
+
const scanResult: ScanResult = {
|
|
163
|
+
repoName: 'test/repo',
|
|
164
|
+
repoUrl: 'https://github.com/test/repo',
|
|
165
|
+
branch: 'main',
|
|
166
|
+
filesScanned: 0,
|
|
167
|
+
filesSkipped: 0,
|
|
168
|
+
vulnerabilities: [],
|
|
169
|
+
severityCounts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
170
|
+
categoryCounts: {},
|
|
171
|
+
hasBlockingIssues: false,
|
|
172
|
+
scanDuration: 100,
|
|
173
|
+
timestamp: '2024-01-15T10:00:00.000Z',
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
expect(existsSync(join(testDir, '.oculum'))).toBe(false)
|
|
177
|
+
|
|
178
|
+
const result = manager.saveBaseline(scanResult)
|
|
179
|
+
|
|
180
|
+
expect(result.success).toBe(true)
|
|
181
|
+
expect(existsSync(join(testDir, '.oculum'))).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('clearBaseline', () => {
|
|
186
|
+
it('should return success with existed: false when no baseline exists', () => {
|
|
187
|
+
const result = manager.clearBaseline()
|
|
188
|
+
expect(result.success).toBe(true)
|
|
189
|
+
expect(result.existed).toBe(false)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should delete existing baseline', () => {
|
|
193
|
+
// Create baseline file
|
|
194
|
+
mkdirSync(join(testDir, '.oculum'), { recursive: true })
|
|
195
|
+
writeFileSync(join(testDir, '.oculum/baseline.json'), '{}')
|
|
196
|
+
|
|
197
|
+
expect(existsSync(join(testDir, '.oculum/baseline.json'))).toBe(true)
|
|
198
|
+
|
|
199
|
+
const result = manager.clearBaseline()
|
|
200
|
+
|
|
201
|
+
expect(result.success).toBe(true)
|
|
202
|
+
expect(result.existed).toBe(true)
|
|
203
|
+
expect(existsSync(join(testDir, '.oculum/baseline.json'))).toBe(false)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('hasBaseline', () => {
|
|
208
|
+
it('should return false when no baseline exists', () => {
|
|
209
|
+
expect(manager.hasBaseline()).toBe(false)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should return true when baseline exists', () => {
|
|
213
|
+
mkdirSync(join(testDir, '.oculum'), { recursive: true })
|
|
214
|
+
writeFileSync(join(testDir, '.oculum/baseline.json'), '{}')
|
|
215
|
+
|
|
216
|
+
expect(manager.hasBaseline()).toBe(true)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe('getBaselinePath', () => {
|
|
221
|
+
it('should return correct path', () => {
|
|
222
|
+
expect(manager.getBaselinePath()).toBe(join(testDir, '.oculum/baseline.json'))
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
})
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline Diff Computation
|
|
3
|
+
* Computes the difference between current findings and a baseline
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Vulnerability, SeverityCounts, VulnerabilitySeverity } from '../types'
|
|
7
|
+
import type { BaselineData, BaselineFinding, DiffResult } from './types'
|
|
8
|
+
import { computeFindingHash } from '../suppression/hash'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute severity counts from baseline findings
|
|
12
|
+
*/
|
|
13
|
+
function computeBaselineSeverityCounts(findings: BaselineFinding[]): SeverityCounts {
|
|
14
|
+
const counts: SeverityCounts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 }
|
|
15
|
+
|
|
16
|
+
for (const finding of findings) {
|
|
17
|
+
const severity = finding.severity as VulnerabilitySeverity
|
|
18
|
+
if (severity in counts) {
|
|
19
|
+
counts[severity]++
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return counts
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compute severity counts from vulnerabilities
|
|
28
|
+
*/
|
|
29
|
+
function computeVulnerabilitySeverityCounts(vulnerabilities: Vulnerability[]): SeverityCounts {
|
|
30
|
+
const counts: SeverityCounts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 }
|
|
31
|
+
|
|
32
|
+
for (const vuln of vulnerabilities) {
|
|
33
|
+
if (vuln.severity in counts) {
|
|
34
|
+
counts[vuln.severity]++
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return counts
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compute the diff between current scan findings and a baseline
|
|
43
|
+
*
|
|
44
|
+
* Uses finding hashes for comparison, which are computed from:
|
|
45
|
+
* - Normalized file path
|
|
46
|
+
* - Normalized line content
|
|
47
|
+
* - Category
|
|
48
|
+
*
|
|
49
|
+
* This means findings are considered the same even if:
|
|
50
|
+
* - Line numbers changed (code moved)
|
|
51
|
+
* - Minor whitespace changes occurred
|
|
52
|
+
*
|
|
53
|
+
* @param currentFindings - Vulnerabilities from the current scan
|
|
54
|
+
* @param baseline - The baseline to compare against
|
|
55
|
+
* @returns DiffResult with new, fixed, and existing findings
|
|
56
|
+
*/
|
|
57
|
+
export function computeDiff(
|
|
58
|
+
currentFindings: Vulnerability[],
|
|
59
|
+
baseline: BaselineData
|
|
60
|
+
): DiffResult {
|
|
61
|
+
// Build hash set from baseline for O(1) lookup
|
|
62
|
+
const baselineHashes = new Set(baseline.findings.map(f => f.hash))
|
|
63
|
+
|
|
64
|
+
// Build hash map from current findings
|
|
65
|
+
const currentHashMap = new Map<string, Vulnerability>()
|
|
66
|
+
for (const finding of currentFindings) {
|
|
67
|
+
const hash = computeFindingHash(finding)
|
|
68
|
+
currentHashMap.set(hash, finding)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Compute new findings (in current, not in baseline)
|
|
72
|
+
const newFindings: Vulnerability[] = []
|
|
73
|
+
for (const finding of currentFindings) {
|
|
74
|
+
const hash = computeFindingHash(finding)
|
|
75
|
+
if (!baselineHashes.has(hash)) {
|
|
76
|
+
newFindings.push(finding)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Compute fixed findings (in baseline, not in current)
|
|
81
|
+
const fixedFindings: BaselineFinding[] = []
|
|
82
|
+
for (const baselineFinding of baseline.findings) {
|
|
83
|
+
if (!currentHashMap.has(baselineFinding.hash)) {
|
|
84
|
+
fixedFindings.push(baselineFinding)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Compute existing findings (in both)
|
|
89
|
+
const existingFindings: Vulnerability[] = []
|
|
90
|
+
for (const finding of currentFindings) {
|
|
91
|
+
const hash = computeFindingHash(finding)
|
|
92
|
+
if (baselineHashes.has(hash)) {
|
|
93
|
+
existingFindings.push(finding)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
new: newFindings,
|
|
99
|
+
fixed: fixedFindings,
|
|
100
|
+
existing: existingFindings,
|
|
101
|
+
stats: {
|
|
102
|
+
newCount: newFindings.length,
|
|
103
|
+
fixedCount: fixedFindings.length,
|
|
104
|
+
existingCount: existingFindings.length,
|
|
105
|
+
newBySeverity: computeVulnerabilitySeverityCounts(newFindings),
|
|
106
|
+
fixedBySeverity: computeBaselineSeverityCounts(fixedFindings),
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if a diff has any new blocking issues (critical or high severity)
|
|
113
|
+
*/
|
|
114
|
+
export function hasNewBlockingIssues(diff: DiffResult): boolean {
|
|
115
|
+
return diff.stats.newBySeverity.critical > 0 || diff.stats.newBySeverity.high > 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Format a summary string for the diff
|
|
120
|
+
*/
|
|
121
|
+
export function formatDiffSummary(diff: DiffResult): string {
|
|
122
|
+
const parts: string[] = []
|
|
123
|
+
|
|
124
|
+
if (diff.stats.newCount > 0) {
|
|
125
|
+
parts.push(`${diff.stats.newCount} new`)
|
|
126
|
+
}
|
|
127
|
+
if (diff.stats.fixedCount > 0) {
|
|
128
|
+
parts.push(`${diff.stats.fixedCount} fixed`)
|
|
129
|
+
}
|
|
130
|
+
if (diff.stats.existingCount > 0) {
|
|
131
|
+
parts.push(`${diff.stats.existingCount} existing`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return parts.join(', ')
|
|
135
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline Module
|
|
3
|
+
* Provides baseline/diff mode functionality for tracking security improvements
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Types
|
|
7
|
+
export type {
|
|
8
|
+
BaselineFinding,
|
|
9
|
+
BaselineData,
|
|
10
|
+
DiffResult,
|
|
11
|
+
BaselineDiff,
|
|
12
|
+
} from './types'
|
|
13
|
+
export { BASELINE_FILE_PATH, OCULUM_DIR } from './types'
|
|
14
|
+
|
|
15
|
+
// Manager
|
|
16
|
+
export {
|
|
17
|
+
BaselineManager,
|
|
18
|
+
type BaselineManagerOptions,
|
|
19
|
+
type LoadBaselineResult,
|
|
20
|
+
type SaveBaselineResult,
|
|
21
|
+
type ClearBaselineResult,
|
|
22
|
+
} from './manager'
|
|
23
|
+
|
|
24
|
+
// Diff computation
|
|
25
|
+
export {
|
|
26
|
+
computeDiff,
|
|
27
|
+
hasNewBlockingIssues,
|
|
28
|
+
formatDiffSummary,
|
|
29
|
+
} from './diff'
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline Manager
|
|
3
|
+
* Handles loading, saving, and clearing baseline files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs'
|
|
7
|
+
import { join, dirname } from 'path'
|
|
8
|
+
import { execFileSync } from 'child_process'
|
|
9
|
+
import type { ScanResult, ScanDepth, Vulnerability } from '../types'
|
|
10
|
+
import type { BaselineData, BaselineFinding } from './types'
|
|
11
|
+
import { BASELINE_FILE_PATH, OCULUM_DIR } from './types'
|
|
12
|
+
import { computeFindingHash } from '../suppression/hash'
|
|
13
|
+
|
|
14
|
+
export interface BaselineManagerOptions {
|
|
15
|
+
/** Project root path */
|
|
16
|
+
projectPath: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LoadBaselineResult {
|
|
20
|
+
/** Whether a baseline was found */
|
|
21
|
+
found: boolean
|
|
22
|
+
/** The baseline data (if found) */
|
|
23
|
+
baseline?: BaselineData
|
|
24
|
+
/** Error message (if failed to load) */
|
|
25
|
+
error?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SaveBaselineResult {
|
|
29
|
+
/** Whether the save was successful */
|
|
30
|
+
success: boolean
|
|
31
|
+
/** Path where baseline was saved */
|
|
32
|
+
path: string
|
|
33
|
+
/** Error message (if failed) */
|
|
34
|
+
error?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ClearBaselineResult {
|
|
38
|
+
/** Whether the clear was successful */
|
|
39
|
+
success: boolean
|
|
40
|
+
/** Whether a baseline existed before clearing */
|
|
41
|
+
existed: boolean
|
|
42
|
+
/** Error message (if failed) */
|
|
43
|
+
error?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get current git commit SHA (short form)
|
|
48
|
+
*/
|
|
49
|
+
function getGitCommit(projectPath: string): string | undefined {
|
|
50
|
+
try {
|
|
51
|
+
const result = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
|
52
|
+
cwd: projectPath,
|
|
53
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
54
|
+
})
|
|
55
|
+
return result.toString().trim()
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get current git branch name
|
|
63
|
+
*/
|
|
64
|
+
function getGitBranch(projectPath: string): string | undefined {
|
|
65
|
+
try {
|
|
66
|
+
const result = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
67
|
+
cwd: projectPath,
|
|
68
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
69
|
+
})
|
|
70
|
+
return result.toString().trim()
|
|
71
|
+
} catch {
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Convert a Vulnerability to a BaselineFinding
|
|
78
|
+
*/
|
|
79
|
+
function toBaselineFinding(vuln: Vulnerability): BaselineFinding {
|
|
80
|
+
return {
|
|
81
|
+
hash: computeFindingHash(vuln),
|
|
82
|
+
filePath: vuln.filePath,
|
|
83
|
+
lineNumber: vuln.lineNumber,
|
|
84
|
+
category: vuln.category,
|
|
85
|
+
severity: vuln.severity,
|
|
86
|
+
title: vuln.title,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Manages baseline files for diff mode
|
|
92
|
+
*/
|
|
93
|
+
export class BaselineManager {
|
|
94
|
+
private projectPath: string
|
|
95
|
+
private baselinePath: string
|
|
96
|
+
|
|
97
|
+
constructor(options: BaselineManagerOptions | string) {
|
|
98
|
+
// Support both old string arg and new options object
|
|
99
|
+
if (typeof options === 'string') {
|
|
100
|
+
this.projectPath = options
|
|
101
|
+
} else {
|
|
102
|
+
this.projectPath = options.projectPath
|
|
103
|
+
}
|
|
104
|
+
this.baselinePath = join(this.projectPath, BASELINE_FILE_PATH)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the full path to the baseline file
|
|
109
|
+
*/
|
|
110
|
+
getBaselinePath(): string {
|
|
111
|
+
return this.baselinePath
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Load baseline from .oculum/baseline.json
|
|
116
|
+
*/
|
|
117
|
+
loadBaseline(): LoadBaselineResult {
|
|
118
|
+
if (!existsSync(this.baselinePath)) {
|
|
119
|
+
return { found: false }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const content = readFileSync(this.baselinePath, 'utf-8')
|
|
124
|
+
const baseline = JSON.parse(content) as BaselineData
|
|
125
|
+
|
|
126
|
+
// Basic validation
|
|
127
|
+
if (baseline.version !== 1) {
|
|
128
|
+
return {
|
|
129
|
+
found: false,
|
|
130
|
+
error: `Unsupported baseline version: ${baseline.version}. Expected version 1.`,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!Array.isArray(baseline.findings)) {
|
|
135
|
+
return {
|
|
136
|
+
found: false,
|
|
137
|
+
error: 'Invalid baseline: missing findings array',
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { found: true, baseline }
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return {
|
|
144
|
+
found: false,
|
|
145
|
+
error: `Failed to parse baseline: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Save current scan result as baseline
|
|
152
|
+
*/
|
|
153
|
+
saveBaseline(
|
|
154
|
+
scanResult: ScanResult,
|
|
155
|
+
options?: { commit?: string; branch?: string; scanDepth?: ScanDepth }
|
|
156
|
+
): SaveBaselineResult {
|
|
157
|
+
try {
|
|
158
|
+
// Ensure .oculum directory exists
|
|
159
|
+
const oculumDir = join(this.projectPath, OCULUM_DIR)
|
|
160
|
+
if (!existsSync(oculumDir)) {
|
|
161
|
+
mkdirSync(oculumDir, { recursive: true })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Get git info if not provided
|
|
165
|
+
const commit = options?.commit ?? getGitCommit(this.projectPath)
|
|
166
|
+
const branch = options?.branch ?? getGitBranch(this.projectPath)
|
|
167
|
+
|
|
168
|
+
// Convert vulnerabilities to baseline findings
|
|
169
|
+
const findings = scanResult.vulnerabilities.map(toBaselineFinding)
|
|
170
|
+
|
|
171
|
+
// Build baseline data
|
|
172
|
+
const baseline: BaselineData = {
|
|
173
|
+
version: 1,
|
|
174
|
+
createdAt: new Date().toISOString(),
|
|
175
|
+
commit,
|
|
176
|
+
branch,
|
|
177
|
+
scanDepth: options?.scanDepth,
|
|
178
|
+
findings,
|
|
179
|
+
stats: {
|
|
180
|
+
total: findings.length,
|
|
181
|
+
critical: scanResult.severityCounts.critical,
|
|
182
|
+
high: scanResult.severityCounts.high,
|
|
183
|
+
medium: scanResult.severityCounts.medium,
|
|
184
|
+
low: scanResult.severityCounts.low,
|
|
185
|
+
info: scanResult.severityCounts.info,
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Write to file
|
|
190
|
+
writeFileSync(this.baselinePath, JSON.stringify(baseline, null, 2))
|
|
191
|
+
|
|
192
|
+
return { success: true, path: this.baselinePath }
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
path: this.baselinePath,
|
|
197
|
+
error: `Failed to save baseline: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Clear (delete) the baseline file
|
|
204
|
+
*/
|
|
205
|
+
clearBaseline(): ClearBaselineResult {
|
|
206
|
+
const existed = existsSync(this.baselinePath)
|
|
207
|
+
|
|
208
|
+
if (!existed) {
|
|
209
|
+
return { success: true, existed: false }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
unlinkSync(this.baselinePath)
|
|
214
|
+
return { success: true, existed: true }
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return {
|
|
217
|
+
success: false,
|
|
218
|
+
existed: true,
|
|
219
|
+
error: `Failed to clear baseline: ${err instanceof Error ? err.message : 'Unknown error'}`,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check if a baseline exists
|
|
226
|
+
*/
|
|
227
|
+
hasBaseline(): boolean {
|
|
228
|
+
return existsSync(this.baselinePath)
|
|
229
|
+
}
|
|
230
|
+
}
|