@oculum/scanner 1.0.11 → 1.0.12
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/ai-context/index.d.ts +6 -0
- package/dist/ai-context/index.d.ts.map +1 -0
- package/dist/ai-context/index.js +13 -0
- package/dist/ai-context/index.js.map +1 -0
- package/dist/ai-context/manager.d.ts +67 -0
- package/dist/ai-context/manager.d.ts.map +1 -0
- package/dist/ai-context/manager.js +104 -0
- package/dist/ai-context/manager.js.map +1 -0
- package/dist/category-filter.d.ts +125 -0
- package/dist/category-filter.d.ts.map +1 -0
- package/dist/category-filter.js +360 -0
- package/dist/category-filter.js.map +1 -0
- package/dist/filtering/context-adjustments.d.ts +23 -0
- package/dist/filtering/context-adjustments.d.ts.map +1 -0
- package/dist/filtering/context-adjustments.js +100 -0
- package/dist/filtering/context-adjustments.js.map +1 -0
- package/dist/filtering/index.d.ts +3 -0
- package/dist/filtering/index.d.ts.map +1 -0
- package/dist/filtering/index.js +8 -0
- package/dist/filtering/index.js.map +1 -0
- package/dist/filtering/pipeline.d.ts +48 -0
- package/dist/filtering/pipeline.d.ts.map +1 -0
- package/dist/filtering/pipeline.js +76 -0
- package/dist/filtering/pipeline.js.map +1 -0
- package/dist/formatters/ai-context.d.ts +23 -0
- package/dist/formatters/ai-context.d.ts.map +1 -0
- package/dist/formatters/ai-context.js +238 -0
- package/dist/formatters/ai-context.js.map +1 -0
- 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 +2 -2
- package/dist/formatters/github-comment.js.map +1 -1
- package/dist/formatters/ide/claude-code.d.ts +17 -0
- package/dist/formatters/ide/claude-code.d.ts.map +1 -0
- package/dist/formatters/ide/claude-code.js +94 -0
- package/dist/formatters/ide/claude-code.js.map +1 -0
- package/dist/formatters/ide/cursor.d.ts +13 -0
- package/dist/formatters/ide/cursor.d.ts.map +1 -0
- package/dist/formatters/ide/cursor.js +125 -0
- package/dist/formatters/ide/cursor.js.map +1 -0
- package/dist/formatters/ide/index.d.ts +62 -0
- package/dist/formatters/ide/index.d.ts.map +1 -0
- package/dist/formatters/ide/index.js +184 -0
- package/dist/formatters/ide/index.js.map +1 -0
- package/dist/formatters/ide/windsurf.d.ts +13 -0
- package/dist/formatters/ide/windsurf.d.ts.map +1 -0
- package/dist/formatters/ide/windsurf.js +117 -0
- package/dist/formatters/ide/windsurf.js.map +1 -0
- package/dist/formatters/index.d.ts +2 -0
- package/dist/formatters/index.d.ts.map +1 -1
- package/dist/formatters/index.js +17 -1
- package/dist/formatters/index.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +272 -44
- package/dist/index.js.map +1 -1
- package/dist/layer1/comments.d.ts +4 -1
- package/dist/layer1/comments.d.ts.map +1 -1
- package/dist/layer1/comments.js +1 -1
- package/dist/layer1/comments.js.map +1 -1
- package/dist/layer1/config-audit.d.ts +4 -1
- package/dist/layer1/config-audit.d.ts.map +1 -1
- package/dist/layer1/config-audit.js +45 -11
- package/dist/layer1/config-audit.js.map +1 -1
- package/dist/layer1/config-mcp-audit.d.ts +4 -1
- package/dist/layer1/config-mcp-audit.d.ts.map +1 -1
- package/dist/layer1/config-mcp-audit.js +2 -2
- package/dist/layer1/config-mcp-audit.js.map +1 -1
- package/dist/layer1/entropy.d.ts +4 -1
- package/dist/layer1/entropy.d.ts.map +1 -1
- package/dist/layer1/entropy.js +212 -1
- package/dist/layer1/entropy.js.map +1 -1
- package/dist/layer1/file-flags.d.ts +4 -1
- package/dist/layer1/file-flags.d.ts.map +1 -1
- package/dist/layer1/file-flags.js +12 -5
- package/dist/layer1/file-flags.js.map +1 -1
- package/dist/layer1/index.d.ts.map +1 -1
- package/dist/layer1/index.js +14 -19
- package/dist/layer1/index.js.map +1 -1
- package/dist/layer1/patterns.d.ts +4 -1
- package/dist/layer1/patterns.d.ts.map +1 -1
- package/dist/layer1/patterns.js +34 -4
- package/dist/layer1/patterns.js.map +1 -1
- package/dist/layer1/urls.d.ts +4 -1
- package/dist/layer1/urls.d.ts.map +1 -1
- package/dist/layer1/urls.js +162 -14
- package/dist/layer1/urls.js.map +1 -1
- package/dist/layer1/weak-crypto.d.ts +4 -1
- package/dist/layer1/weak-crypto.d.ts.map +1 -1
- package/dist/layer1/weak-crypto.js +144 -7
- package/dist/layer1/weak-crypto.js.map +1 -1
- package/dist/layer2/ai-agent-tools.d.ts +4 -1
- package/dist/layer2/ai-agent-tools.d.ts.map +1 -1
- package/dist/layer2/ai-agent-tools.js +661 -2
- package/dist/layer2/ai-agent-tools.js.map +1 -1
- package/dist/layer2/ai-endpoint-protection.d.ts +2 -0
- package/dist/layer2/ai-endpoint-protection.d.ts.map +1 -1
- package/dist/layer2/ai-endpoint-protection.js +1 -1
- package/dist/layer2/ai-endpoint-protection.js.map +1 -1
- package/dist/layer2/ai-execution-sinks.d.ts +4 -1
- package/dist/layer2/ai-execution-sinks.d.ts.map +1 -1
- package/dist/layer2/ai-execution-sinks.js +252 -43
- package/dist/layer2/ai-execution-sinks.js.map +1 -1
- package/dist/layer2/ai-fingerprinting.d.ts +4 -1
- package/dist/layer2/ai-fingerprinting.d.ts.map +1 -1
- package/dist/layer2/ai-fingerprinting.js +25 -32
- package/dist/layer2/ai-fingerprinting.js.map +1 -1
- package/dist/layer2/ai-mcp-security.d.ts +4 -1
- package/dist/layer2/ai-mcp-security.d.ts.map +1 -1
- package/dist/layer2/ai-mcp-security.js +200 -2
- package/dist/layer2/ai-mcp-security.js.map +1 -1
- package/dist/layer2/ai-package-hallucination.d.ts +4 -1
- package/dist/layer2/ai-package-hallucination.d.ts.map +1 -1
- package/dist/layer2/ai-package-hallucination.js +136 -4
- package/dist/layer2/ai-package-hallucination.js.map +1 -1
- package/dist/layer2/ai-prompt-hygiene.d.ts +4 -1
- package/dist/layer2/ai-prompt-hygiene.d.ts.map +1 -1
- package/dist/layer2/ai-prompt-hygiene.js +342 -28
- package/dist/layer2/ai-prompt-hygiene.js.map +1 -1
- package/dist/layer2/ai-rag-safety.d.ts +4 -1
- package/dist/layer2/ai-rag-safety.d.ts.map +1 -1
- package/dist/layer2/ai-rag-safety.js +82 -2
- package/dist/layer2/ai-rag-safety.js.map +1 -1
- package/dist/layer2/ai-schema-validation.d.ts +4 -1
- package/dist/layer2/ai-schema-validation.d.ts.map +1 -1
- package/dist/layer2/ai-schema-validation.js +2 -2
- package/dist/layer2/ai-schema-validation.js.map +1 -1
- package/dist/layer2/auth-antipatterns.d.ts +2 -0
- package/dist/layer2/auth-antipatterns.d.ts.map +1 -1
- package/dist/layer2/auth-antipatterns.js +205 -20
- package/dist/layer2/auth-antipatterns.js.map +1 -1
- package/dist/layer2/byok-patterns.d.ts +4 -1
- package/dist/layer2/byok-patterns.d.ts.map +1 -1
- package/dist/layer2/byok-patterns.js +2 -2
- package/dist/layer2/byok-patterns.js.map +1 -1
- package/dist/layer2/dangerous-functions/dom-xss.d.ts +9 -4
- package/dist/layer2/dangerous-functions/dom-xss.d.ts.map +1 -1
- package/dist/layer2/dangerous-functions/dom-xss.js +73 -22
- package/dist/layer2/dangerous-functions/dom-xss.js.map +1 -1
- package/dist/layer2/dangerous-functions/index.d.ts +4 -1
- package/dist/layer2/dangerous-functions/index.d.ts.map +1 -1
- package/dist/layer2/dangerous-functions/index.js +551 -20
- package/dist/layer2/dangerous-functions/index.js.map +1 -1
- package/dist/layer2/dangerous-functions/math-random.d.ts +54 -4
- package/dist/layer2/dangerous-functions/math-random.d.ts.map +1 -1
- package/dist/layer2/dangerous-functions/math-random.js +241 -16
- package/dist/layer2/dangerous-functions/math-random.js.map +1 -1
- package/dist/layer2/dangerous-functions/patterns.d.ts.map +1 -1
- package/dist/layer2/dangerous-functions/patterns.js +3 -1
- package/dist/layer2/dangerous-functions/patterns.js.map +1 -1
- package/dist/layer2/dangerous-functions/utils/control-flow.d.ts +3 -2
- package/dist/layer2/dangerous-functions/utils/control-flow.d.ts.map +1 -1
- package/dist/layer2/dangerous-functions/utils/control-flow.js +41 -120
- package/dist/layer2/dangerous-functions/utils/control-flow.js.map +1 -1
- package/dist/layer2/dangerous-functions/utils/helpers.d.ts.map +1 -1
- package/dist/layer2/dangerous-functions/utils/helpers.js +26 -3
- package/dist/layer2/dangerous-functions/utils/helpers.js.map +1 -1
- package/dist/layer2/dangerous-functions/utils/schema-validation.d.ts.map +1 -1
- package/dist/layer2/dangerous-functions/utils/schema-validation.js +14 -1
- package/dist/layer2/dangerous-functions/utils/schema-validation.js.map +1 -1
- package/dist/layer2/data-exposure.d.ts +4 -1
- package/dist/layer2/data-exposure.d.ts.map +1 -1
- package/dist/layer2/data-exposure.js +11 -38
- package/dist/layer2/data-exposure.js.map +1 -1
- package/dist/layer2/framework-checks.d.ts +4 -1
- package/dist/layer2/framework-checks.d.ts.map +1 -1
- package/dist/layer2/framework-checks.js +2 -2
- package/dist/layer2/framework-checks.js.map +1 -1
- package/dist/layer2/index.d.ts +9 -1
- package/dist/layer2/index.d.ts.map +1 -1
- package/dist/layer2/index.js +57 -51
- package/dist/layer2/index.js.map +1 -1
- package/dist/layer2/logic-gates.d.ts +4 -1
- package/dist/layer2/logic-gates.d.ts.map +1 -1
- package/dist/layer2/logic-gates.js +54 -20
- package/dist/layer2/logic-gates.js.map +1 -1
- package/dist/layer2/model-supply-chain.d.ts +4 -1
- package/dist/layer2/model-supply-chain.d.ts.map +1 -1
- package/dist/layer2/model-supply-chain.js +72 -4
- package/dist/layer2/model-supply-chain.js.map +1 -1
- package/dist/layer2/risky-imports.d.ts +4 -1
- package/dist/layer2/risky-imports.d.ts.map +1 -1
- package/dist/layer2/risky-imports.js +2 -2
- package/dist/layer2/risky-imports.js.map +1 -1
- package/dist/layer2/variables.d.ts +4 -1
- package/dist/layer2/variables.d.ts.map +1 -1
- package/dist/layer2/variables.js +2 -2
- package/dist/layer2/variables.js.map +1 -1
- package/dist/layer3/anthropic/auto-dismiss.d.ts.map +1 -1
- package/dist/layer3/anthropic/auto-dismiss.js +11 -0
- package/dist/layer3/anthropic/auto-dismiss.js.map +1 -1
- package/dist/modes/incremental.js +1 -1
- package/dist/tiers.d.ts +2 -2
- package/dist/tiers.d.ts.map +1 -1
- package/dist/tiers.js +7 -7
- package/dist/tiers.js.map +1 -1
- package/dist/types.d.ts +78 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +34 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/code-analysis.d.ts +39 -0
- package/dist/utils/code-analysis.d.ts.map +1 -0
- package/dist/utils/code-analysis.js +159 -0
- package/dist/utils/code-analysis.js.map +1 -0
- package/dist/utils/comment-analyzer.d.ts +38 -0
- package/dist/utils/comment-analyzer.d.ts.map +1 -0
- package/dist/utils/comment-analyzer.js +218 -0
- package/dist/utils/comment-analyzer.js.map +1 -0
- package/dist/utils/context-helpers.d.ts +108 -1
- package/dist/utils/context-helpers.d.ts.map +1 -1
- package/dist/utils/context-helpers.js +351 -2
- package/dist/utils/context-helpers.js.map +1 -1
- package/dist/utils/environment-context.d.ts +76 -0
- package/dist/utils/environment-context.d.ts.map +1 -0
- package/dist/utils/environment-context.js +271 -0
- package/dist/utils/environment-context.js.map +1 -0
- package/dist/utils/intent-detector.d.ts +66 -0
- package/dist/utils/intent-detector.d.ts.map +1 -0
- package/dist/utils/intent-detector.js +282 -0
- package/dist/utils/intent-detector.js.map +1 -0
- package/dist/utils/parsed-file.d.ts +51 -0
- package/dist/utils/parsed-file.d.ts.map +1 -0
- package/dist/utils/parsed-file.js +95 -0
- package/dist/utils/parsed-file.js.map +1 -0
- package/dist/utils/route-hierarchy.d.ts +50 -0
- package/dist/utils/route-hierarchy.d.ts.map +1 -0
- package/dist/utils/route-hierarchy.js +226 -0
- package/dist/utils/route-hierarchy.js.map +1 -0
- package/dist/utils/schema-semantics.d.ts +45 -0
- package/dist/utils/schema-semantics.d.ts.map +1 -0
- package/dist/utils/schema-semantics.js +193 -0
- package/dist/utils/schema-semantics.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/benchmark/fixtures/layer2/index.ts +12 -0
- package/src/__tests__/benchmark/fixtures/layer2/phase5-excessive-agency.ts +580 -0
- package/src/__tests__/benchmark/fixtures/layer2/sprint6-ai-enhancements.ts +515 -0
- package/src/__tests__/benchmark/run-depth-validation.ts +9 -9
- package/src/__tests__/category-filter.test.ts +478 -0
- package/src/__tests__/regression/known-false-positives.test.ts +490 -0
- package/src/__tests__/snapshots/__snapshots__/anthropic-validation-refactor.test.ts.snap +18 -14
- package/src/__tests__/snapshots/__snapshots__/scan-depth.test.ts.snap +0 -9
- package/src/__tests__/snapshots/anthropic-validation-refactor.test.ts +1 -1
- package/src/__tests__/validation/run-validation.ts +7 -7
- package/src/ai-context/__tests__/manager.test.ts +193 -0
- package/src/ai-context/index.ts +15 -0
- package/src/ai-context/manager.ts +145 -0
- package/src/baseline/__tests__/manager.test.ts +2 -2
- package/src/category-filter.ts +400 -0
- package/src/filtering/__tests__/pipeline.test.ts +134 -0
- package/src/filtering/context-adjustments.ts +111 -0
- package/src/filtering/index.ts +10 -0
- package/src/filtering/pipeline.ts +130 -0
- package/src/formatters/__tests__/ai-context.test.ts +254 -0
- package/src/formatters/ai-context.ts +302 -0
- package/src/formatters/github-comment.ts +3 -3
- package/src/formatters/ide/__tests__/ide.test.ts +319 -0
- package/src/formatters/ide/claude-code.ts +110 -0
- package/src/formatters/ide/cursor.ts +147 -0
- package/src/formatters/ide/index.ts +216 -0
- package/src/formatters/ide/windsurf.ts +135 -0
- package/src/formatters/index.ts +24 -0
- package/src/index.ts +312 -34
- package/src/layer1/comments.ts +3 -1
- package/src/layer1/config-audit.ts +50 -11
- package/src/layer1/config-mcp-audit.ts +4 -2
- package/src/layer1/entropy.ts +234 -1
- package/src/layer1/file-flags.ts +17 -6
- package/src/layer1/index.ts +14 -18
- package/src/layer1/patterns.ts +42 -4
- package/src/layer1/urls.ts +188 -14
- package/src/layer1/weak-crypto.ts +168 -16
- package/src/layer2/ai-agent-tools.ts +707 -2
- package/src/layer2/ai-endpoint-protection.ts +3 -1
- package/src/layer2/ai-execution-sinks.ts +265 -43
- package/src/layer2/ai-fingerprinting.ts +28 -32
- package/src/layer2/ai-mcp-security.ts +206 -3
- package/src/layer2/ai-package-hallucination.ts +153 -4
- package/src/layer2/ai-prompt-hygiene.ts +369 -26
- package/src/layer2/ai-rag-safety.ts +85 -2
- package/src/layer2/ai-schema-validation.ts +4 -2
- package/src/layer2/auth-antipatterns.ts +230 -20
- package/src/layer2/byok-patterns.ts +4 -2
- package/src/layer2/dangerous-functions/dom-xss.ts +94 -22
- package/src/layer2/dangerous-functions/index.ts +635 -51
- package/src/layer2/dangerous-functions/math-random.ts +268 -16
- package/src/layer2/dangerous-functions/patterns.ts +3 -1
- package/src/layer2/dangerous-functions/utils/control-flow.ts +8 -135
- package/src/layer2/dangerous-functions/utils/schema-validation.ts +16 -1
- package/src/layer2/data-exposure.ts +13 -38
- package/src/layer2/framework-checks.ts +4 -2
- package/src/layer2/index.ts +69 -50
- package/src/layer2/logic-gates.ts +59 -22
- package/src/layer2/model-supply-chain.ts +79 -4
- package/src/layer2/risky-imports.ts +4 -2
- package/src/layer2/variables.ts +4 -2
- package/src/layer3/anthropic/auto-dismiss.ts +11 -0
- package/src/modes/incremental.ts +1 -1
- package/src/tiers.ts +9 -9
- package/src/types.ts +122 -8
- package/src/utils/__tests__/code-analysis.test.ts +165 -0
- package/src/utils/__tests__/parsed-file.test.ts +124 -0
- package/src/utils/code-analysis.ts +179 -0
- package/src/utils/comment-analyzer.ts +249 -0
- package/src/utils/context-helpers.ts +408 -2
- package/src/utils/environment-context.ts +304 -0
- package/src/utils/intent-detector.ts +318 -0
- package/src/utils/parsed-file.ts +103 -0
- package/src/utils/route-hierarchy.ts +250 -0
- package/src/utils/schema-semantics.ts +233 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE Integration Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync, writeFileSync, rmSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
import { tmpdir } from 'os'
|
|
8
|
+
import {
|
|
9
|
+
formatCursorRules,
|
|
10
|
+
formatWindsurfRules,
|
|
11
|
+
formatClaudeCodeSection,
|
|
12
|
+
detectIDEConfigs,
|
|
13
|
+
writeIDEFile,
|
|
14
|
+
updateClaudeMdSection,
|
|
15
|
+
clearIDEFiles,
|
|
16
|
+
} from '../index'
|
|
17
|
+
import type { ScanResult, Vulnerability } from '../../../types'
|
|
18
|
+
|
|
19
|
+
const createMockVulnerability = (overrides: Partial<Vulnerability> = {}): Vulnerability => ({
|
|
20
|
+
id: 'test-vuln-1',
|
|
21
|
+
filePath: 'src/api/users.ts',
|
|
22
|
+
lineNumber: 42,
|
|
23
|
+
lineContent: 'const query = `SELECT * FROM users WHERE id = ${userId}`',
|
|
24
|
+
severity: 'high',
|
|
25
|
+
category: 'sql_injection',
|
|
26
|
+
title: 'SQL Injection Vulnerability',
|
|
27
|
+
description: 'User input is directly concatenated into SQL query without parameterization.',
|
|
28
|
+
suggestedFix: 'Use parameterized queries instead of string concatenation.',
|
|
29
|
+
confidence: 'high',
|
|
30
|
+
layer: 2,
|
|
31
|
+
fixSteps: [
|
|
32
|
+
'Replace string concatenation with parameterized query',
|
|
33
|
+
'Use prepared statements or an ORM',
|
|
34
|
+
],
|
|
35
|
+
...overrides,
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const createMockScanResult = (vulnerabilities: Vulnerability[] = []): ScanResult => ({
|
|
39
|
+
repoName: 'test-repo',
|
|
40
|
+
repoUrl: 'https://github.com/test/test-repo',
|
|
41
|
+
branch: 'main',
|
|
42
|
+
filesScanned: 50,
|
|
43
|
+
filesSkipped: 5,
|
|
44
|
+
vulnerabilities,
|
|
45
|
+
severityCounts: {
|
|
46
|
+
critical: vulnerabilities.filter(v => v.severity === 'critical').length,
|
|
47
|
+
high: vulnerabilities.filter(v => v.severity === 'high').length,
|
|
48
|
+
medium: vulnerabilities.filter(v => v.severity === 'medium').length,
|
|
49
|
+
low: vulnerabilities.filter(v => v.severity === 'low').length,
|
|
50
|
+
info: vulnerabilities.filter(v => v.severity === 'info').length,
|
|
51
|
+
},
|
|
52
|
+
categoryCounts: {},
|
|
53
|
+
hasBlockingIssues: vulnerabilities.some(v => v.severity === 'critical' || v.severity === 'high'),
|
|
54
|
+
scanDuration: 1500,
|
|
55
|
+
timestamp: '2024-01-15T10:30:00.000Z',
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('formatCursorRules', () => {
|
|
59
|
+
it('should generate MDC format with frontmatter', () => {
|
|
60
|
+
const vuln = createMockVulnerability()
|
|
61
|
+
const result = createMockScanResult([vuln])
|
|
62
|
+
|
|
63
|
+
const output = formatCursorRules(result)
|
|
64
|
+
|
|
65
|
+
// Check frontmatter
|
|
66
|
+
expect(output).toContain('---')
|
|
67
|
+
expect(output).toContain('description: Oculum Security Findings')
|
|
68
|
+
expect(output).toContain('alwaysApply: false')
|
|
69
|
+
expect(output).toContain('---')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should include glob patterns for affected files', () => {
|
|
73
|
+
const vulns = [
|
|
74
|
+
createMockVulnerability({ filePath: 'src/api/users.ts' }),
|
|
75
|
+
createMockVulnerability({ id: 'v2', filePath: 'src/api/orders.ts' }),
|
|
76
|
+
createMockVulnerability({ id: 'v3', filePath: 'src/config/db.ts' }),
|
|
77
|
+
]
|
|
78
|
+
const result = createMockScanResult(vulns)
|
|
79
|
+
|
|
80
|
+
const output = formatCursorRules(result)
|
|
81
|
+
|
|
82
|
+
// Should include globs for affected directories
|
|
83
|
+
expect(output).toContain('globs:')
|
|
84
|
+
expect(output).toContain('src/api/**')
|
|
85
|
+
expect(output).toContain('src/config/**')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should include security findings', () => {
|
|
89
|
+
const vuln = createMockVulnerability()
|
|
90
|
+
const result = createMockScanResult([vuln])
|
|
91
|
+
|
|
92
|
+
const output = formatCursorRules(result)
|
|
93
|
+
|
|
94
|
+
expect(output).toContain('SQL Injection Vulnerability')
|
|
95
|
+
expect(output).toContain('src/api/users.ts:42')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should include fix instructions', () => {
|
|
99
|
+
const vuln = createMockVulnerability()
|
|
100
|
+
const result = createMockScanResult([vuln])
|
|
101
|
+
|
|
102
|
+
const output = formatCursorRules(result)
|
|
103
|
+
|
|
104
|
+
expect(output).toContain('DO NOT')
|
|
105
|
+
// The fix shows example code with '?' instead of the word 'parameterized'
|
|
106
|
+
expect(output).toContain('GOOD')
|
|
107
|
+
expect(output).toContain("'SELECT * FROM users WHERE id = ?'")
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should return minimal output for zero findings', () => {
|
|
111
|
+
const result = createMockScanResult([])
|
|
112
|
+
|
|
113
|
+
const output = formatCursorRules(result)
|
|
114
|
+
|
|
115
|
+
expect(output).toContain('description: Oculum Security Findings')
|
|
116
|
+
expect(output).toContain('No security issues found')
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('formatWindsurfRules', () => {
|
|
121
|
+
it('should generate markdown security rules', () => {
|
|
122
|
+
const vuln = createMockVulnerability()
|
|
123
|
+
const result = createMockScanResult([vuln])
|
|
124
|
+
|
|
125
|
+
const output = formatWindsurfRules(result)
|
|
126
|
+
|
|
127
|
+
expect(output).toContain('# Oculum Security Rules')
|
|
128
|
+
expect(output).toContain('## Security Issues')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('should list findings by file', () => {
|
|
132
|
+
const vulns = [
|
|
133
|
+
createMockVulnerability({ filePath: 'src/api/users.ts' }),
|
|
134
|
+
createMockVulnerability({ id: 'v2', filePath: 'src/api/orders.ts' }),
|
|
135
|
+
]
|
|
136
|
+
const result = createMockScanResult(vulns)
|
|
137
|
+
|
|
138
|
+
const output = formatWindsurfRules(result)
|
|
139
|
+
|
|
140
|
+
expect(output).toContain('src/api/users.ts')
|
|
141
|
+
expect(output).toContain('src/api/orders.ts')
|
|
142
|
+
expect(output).toContain('Line 42')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should include code patterns section', () => {
|
|
146
|
+
const vuln = createMockVulnerability()
|
|
147
|
+
const result = createMockScanResult([vuln])
|
|
148
|
+
|
|
149
|
+
const output = formatWindsurfRules(result)
|
|
150
|
+
|
|
151
|
+
expect(output).toContain('## Code Patterns')
|
|
152
|
+
expect(output).toContain('ALWAYS')
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('formatClaudeCodeSection', () => {
|
|
157
|
+
it('should return section with markers', () => {
|
|
158
|
+
const vuln = createMockVulnerability()
|
|
159
|
+
const result = createMockScanResult([vuln])
|
|
160
|
+
|
|
161
|
+
const output = formatClaudeCodeSection(result)
|
|
162
|
+
|
|
163
|
+
expect(output).toContain('<!-- OCULUM_SECURITY_START -->')
|
|
164
|
+
expect(output).toContain('<!-- OCULUM_SECURITY_END -->')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should include security issues', () => {
|
|
168
|
+
const vuln = createMockVulnerability()
|
|
169
|
+
const result = createMockScanResult([vuln])
|
|
170
|
+
|
|
171
|
+
const output = formatClaudeCodeSection(result)
|
|
172
|
+
|
|
173
|
+
expect(output).toContain('## Security Issues')
|
|
174
|
+
expect(output).toContain('SQL Injection')
|
|
175
|
+
expect(output).toContain('src/api/users.ts:42')
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should include verification instructions', () => {
|
|
179
|
+
const vuln = createMockVulnerability()
|
|
180
|
+
const result = createMockScanResult([vuln])
|
|
181
|
+
|
|
182
|
+
const output = formatClaudeCodeSection(result)
|
|
183
|
+
|
|
184
|
+
expect(output).toContain('oculum scan')
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
describe('detectIDEConfigs', () => {
|
|
189
|
+
const testDir = join(tmpdir(), 'oculum-ide-detect-test-' + Date.now())
|
|
190
|
+
|
|
191
|
+
beforeAll(() => {
|
|
192
|
+
mkdirSync(testDir, { recursive: true })
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
afterAll(() => {
|
|
196
|
+
try {
|
|
197
|
+
rmSync(testDir, { recursive: true, force: true })
|
|
198
|
+
} catch {
|
|
199
|
+
// Ignore cleanup errors
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should detect .cursor directory', () => {
|
|
204
|
+
// Create .cursor directory
|
|
205
|
+
mkdirSync(join(testDir, '.cursor'), { recursive: true })
|
|
206
|
+
|
|
207
|
+
const detected = detectIDEConfigs(testDir)
|
|
208
|
+
|
|
209
|
+
expect(detected).toContain('cursor')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should detect CLAUDE.md file', () => {
|
|
213
|
+
// Create CLAUDE.md file
|
|
214
|
+
writeFileSync(join(testDir, 'CLAUDE.md'), '# Claude Code\n')
|
|
215
|
+
|
|
216
|
+
const detected = detectIDEConfigs(testDir)
|
|
217
|
+
|
|
218
|
+
expect(detected).toContain('claude-code')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should return array of detected IDEs', () => {
|
|
222
|
+
const detected = detectIDEConfigs(testDir)
|
|
223
|
+
|
|
224
|
+
expect(Array.isArray(detected)).toBe(true)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe('IDE file operations', () => {
|
|
229
|
+
const testDir = join(tmpdir(), 'oculum-ide-ops-test-' + Date.now())
|
|
230
|
+
|
|
231
|
+
beforeAll(() => {
|
|
232
|
+
mkdirSync(testDir, { recursive: true })
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
afterAll(() => {
|
|
236
|
+
try {
|
|
237
|
+
rmSync(testDir, { recursive: true, force: true })
|
|
238
|
+
} catch {
|
|
239
|
+
// Ignore cleanup errors
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should write IDE file and create directories', () => {
|
|
244
|
+
const result = writeIDEFile(
|
|
245
|
+
testDir,
|
|
246
|
+
'.cursor/rules/security.mdc',
|
|
247
|
+
'# Security Rules'
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
expect(result.success).toBe(true)
|
|
251
|
+
expect(existsSync(join(testDir, '.cursor/rules/security.mdc'))).toBe(true)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('should update CLAUDE.md section between markers', () => {
|
|
255
|
+
// Create initial CLAUDE.md
|
|
256
|
+
const initialContent = `# Project
|
|
257
|
+
|
|
258
|
+
Some existing content.
|
|
259
|
+
|
|
260
|
+
<!-- OCULUM_SECURITY_START -->
|
|
261
|
+
Old security content
|
|
262
|
+
<!-- OCULUM_SECURITY_END -->
|
|
263
|
+
|
|
264
|
+
More content.
|
|
265
|
+
`
|
|
266
|
+
writeFileSync(join(testDir, 'CLAUDE.md'), initialContent)
|
|
267
|
+
|
|
268
|
+
// Update section
|
|
269
|
+
const result = updateClaudeMdSection(
|
|
270
|
+
testDir,
|
|
271
|
+
'<!-- OCULUM_SECURITY_START -->\nNew security content\n<!-- OCULUM_SECURITY_END -->'
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
expect(result.success).toBe(true)
|
|
275
|
+
|
|
276
|
+
// Read and verify
|
|
277
|
+
const { readFileSync } = require('fs')
|
|
278
|
+
const updated = readFileSync(join(testDir, 'CLAUDE.md'), 'utf-8')
|
|
279
|
+
|
|
280
|
+
expect(updated).toContain('New security content')
|
|
281
|
+
expect(updated).not.toContain('Old security content')
|
|
282
|
+
expect(updated).toContain('Some existing content')
|
|
283
|
+
expect(updated).toContain('More content')
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('should append section if no markers exist', () => {
|
|
287
|
+
// Create CLAUDE.md without markers
|
|
288
|
+
const noMarkersDir = join(testDir, 'no-markers')
|
|
289
|
+
mkdirSync(noMarkersDir, { recursive: true })
|
|
290
|
+
writeFileSync(join(noMarkersDir, 'CLAUDE.md'), '# Project\n\nSome content.\n')
|
|
291
|
+
|
|
292
|
+
const result = updateClaudeMdSection(
|
|
293
|
+
noMarkersDir,
|
|
294
|
+
'<!-- OCULUM_SECURITY_START -->\nSecurity content\n<!-- OCULUM_SECURITY_END -->'
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
expect(result.success).toBe(true)
|
|
298
|
+
|
|
299
|
+
const { readFileSync } = require('fs')
|
|
300
|
+
const updated = readFileSync(join(noMarkersDir, 'CLAUDE.md'), 'utf-8')
|
|
301
|
+
|
|
302
|
+
expect(updated).toContain('Security content')
|
|
303
|
+
expect(updated).toContain('Some content')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should clear IDE files', () => {
|
|
307
|
+
// Create some IDE files
|
|
308
|
+
const clearDir = join(testDir, 'clear-test')
|
|
309
|
+
mkdirSync(join(clearDir, '.cursor/rules'), { recursive: true })
|
|
310
|
+
writeFileSync(join(clearDir, '.cursor/rules/security.mdc'), 'content')
|
|
311
|
+
writeFileSync(join(clearDir, '.windsurfrules'), 'content')
|
|
312
|
+
|
|
313
|
+
const result = clearIDEFiles(clearDir)
|
|
314
|
+
|
|
315
|
+
expect(result.success).toBe(true)
|
|
316
|
+
expect(existsSync(join(clearDir, '.cursor/rules/security.mdc'))).toBe(false)
|
|
317
|
+
expect(existsSync(join(clearDir, '.windsurfrules'))).toBe(false)
|
|
318
|
+
})
|
|
319
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code IDE Integration
|
|
3
|
+
* Generates CLAUDE.md section format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ScanResult, Vulnerability, VulnerabilityCategory } from '../../types'
|
|
7
|
+
import { sortBySeverity } from '../grouping'
|
|
8
|
+
|
|
9
|
+
/** Start marker for Oculum section in CLAUDE.md */
|
|
10
|
+
export const OCULUM_SECTION_START = '<!-- OCULUM_SECURITY_START -->'
|
|
11
|
+
|
|
12
|
+
/** End marker for Oculum section in CLAUDE.md */
|
|
13
|
+
export const OCULUM_SECTION_END = '<!-- OCULUM_SECURITY_END -->'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get "ALWAYS" rule for a category
|
|
17
|
+
*/
|
|
18
|
+
function getAlwaysRule(category: VulnerabilityCategory): string | null {
|
|
19
|
+
const rules: Partial<Record<VulnerabilityCategory, string>> = {
|
|
20
|
+
sql_injection: 'Use parameterized queries for all database operations',
|
|
21
|
+
xss: 'Escape or sanitize user input before rendering in HTML',
|
|
22
|
+
hardcoded_secret: 'Use environment variables for secrets and API keys',
|
|
23
|
+
high_entropy_string: 'Store credentials in secure vaults, not in code',
|
|
24
|
+
missing_auth: 'Add authentication middleware to API endpoints',
|
|
25
|
+
dangerous_function: 'Avoid eval() and exec() - use safe alternatives',
|
|
26
|
+
command_injection: 'Sanitize all input passed to shell commands',
|
|
27
|
+
data_exposure: 'Never log sensitive data or expose it in responses',
|
|
28
|
+
weak_crypto: 'Use modern cryptographic algorithms (SHA-256+, AES-256)',
|
|
29
|
+
ai_prompt_injection: 'Sanitize user input before including in prompts',
|
|
30
|
+
ai_unsafe_execution: 'Validate AI-generated code before execution',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return rules[category] || null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Format scan result as CLAUDE.md section
|
|
38
|
+
*
|
|
39
|
+
* @param result - The scan result to format
|
|
40
|
+
* @returns Markdown string with markers for CLAUDE.md
|
|
41
|
+
*/
|
|
42
|
+
export function formatClaudeCodeSection(result: ScanResult): string {
|
|
43
|
+
const { vulnerabilities, timestamp } = result
|
|
44
|
+
|
|
45
|
+
// Sort by severity
|
|
46
|
+
const sorted = sortBySeverity(vulnerabilities)
|
|
47
|
+
|
|
48
|
+
let md = ''
|
|
49
|
+
|
|
50
|
+
// Section markers
|
|
51
|
+
md += `${OCULUM_SECTION_START}\n`
|
|
52
|
+
md += `## Security Issues (Auto-Generated)\n\n`
|
|
53
|
+
md += `> Last scan: ${timestamp}\n\n`
|
|
54
|
+
|
|
55
|
+
// No findings case
|
|
56
|
+
if (vulnerabilities.length === 0) {
|
|
57
|
+
md += `No security issues detected.\n\n`
|
|
58
|
+
md += `Run \`oculum scan\` to scan for vulnerabilities.\n`
|
|
59
|
+
md += `${OCULUM_SECTION_END}\n`
|
|
60
|
+
return md
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Summary
|
|
64
|
+
const { severityCounts, hasBlockingIssues } = result
|
|
65
|
+
if (hasBlockingIssues) {
|
|
66
|
+
md += `**BLOCKING ISSUES:** ${severityCounts.critical + severityCounts.high} issues must be fixed.\n\n`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// DO NOT section - list findings
|
|
70
|
+
md += `**DO NOT** ignore these findings:\n\n`
|
|
71
|
+
|
|
72
|
+
let count = 0
|
|
73
|
+
for (const vuln of sorted.slice(0, 10)) {
|
|
74
|
+
count++
|
|
75
|
+
const severityLabel =
|
|
76
|
+
vuln.severity === 'critical' ? 'CRITICAL' :
|
|
77
|
+
vuln.severity === 'high' ? 'HIGH' :
|
|
78
|
+
vuln.severity.toUpperCase()
|
|
79
|
+
|
|
80
|
+
md += `${count}. **${vuln.title}** [${severityLabel}] in \`${vuln.filePath}:${vuln.lineNumber}\`\n`
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (sorted.length > 10) {
|
|
84
|
+
md += `\n... and ${sorted.length - 10} more issues.\n`
|
|
85
|
+
}
|
|
86
|
+
md += '\n'
|
|
87
|
+
|
|
88
|
+
// ALWAYS section - best practices
|
|
89
|
+
const alwaysRules = new Set<string>()
|
|
90
|
+
for (const vuln of vulnerabilities) {
|
|
91
|
+
const rule = getAlwaysRule(vuln.category)
|
|
92
|
+
if (rule) {
|
|
93
|
+
alwaysRules.add(rule)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (alwaysRules.size > 0) {
|
|
98
|
+
md += `**ALWAYS:**\n`
|
|
99
|
+
for (const rule of alwaysRules) {
|
|
100
|
+
md += `- ${rule}\n`
|
|
101
|
+
}
|
|
102
|
+
md += '\n'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Verification instructions
|
|
106
|
+
md += `Run \`oculum scan\` to verify fixes.\n`
|
|
107
|
+
md += `${OCULUM_SECTION_END}\n`
|
|
108
|
+
|
|
109
|
+
return md
|
|
110
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor IDE Integration
|
|
3
|
+
* Generates .cursor/rules/security.mdc format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ScanResult, Vulnerability } from '../../types'
|
|
7
|
+
import { sortBySeverity } from '../grouping'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extract unique directory paths from vulnerabilities for glob patterns
|
|
11
|
+
*/
|
|
12
|
+
function extractGlobPatterns(vulnerabilities: Vulnerability[]): string[] {
|
|
13
|
+
const directories = new Set<string>()
|
|
14
|
+
|
|
15
|
+
for (const vuln of vulnerabilities) {
|
|
16
|
+
// Get directory path (e.g., 'src/api' from 'src/api/users.ts')
|
|
17
|
+
const parts = vuln.filePath.split('/')
|
|
18
|
+
if (parts.length > 1) {
|
|
19
|
+
// Take up to 2 levels of directory
|
|
20
|
+
const dir = parts.slice(0, Math.min(parts.length - 1, 2)).join('/')
|
|
21
|
+
directories.add(`${dir}/**`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Array.from(directories).sort()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format fix pattern (DO NOT / DO) for a vulnerability
|
|
30
|
+
*/
|
|
31
|
+
function formatFixPattern(vuln: Vulnerability): string {
|
|
32
|
+
const category = vuln.category
|
|
33
|
+
let md = ''
|
|
34
|
+
|
|
35
|
+
// Common patterns by category
|
|
36
|
+
switch (category) {
|
|
37
|
+
case 'sql_injection':
|
|
38
|
+
md += `**DO NOT** concatenate user input into SQL queries.\n\n`
|
|
39
|
+
md += `\`\`\`typescript\n// BAD\nconst query = \`SELECT * FROM users WHERE id = \${userId}\`\n\n// GOOD\nconst query = 'SELECT * FROM users WHERE id = ?'\n\`\`\`\n`
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
case 'xss':
|
|
43
|
+
md += `**DO NOT** insert user input into HTML without escaping.\n\n`
|
|
44
|
+
md += `\`\`\`typescript\n// BAD\nelement.innerHTML = userInput\n\n// GOOD\nelement.textContent = userInput\n\`\`\`\n`
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
case 'hardcoded_secret':
|
|
48
|
+
case 'high_entropy_string':
|
|
49
|
+
md += `**DO NOT** hardcode secrets or API keys in source code.\n\n`
|
|
50
|
+
md += `\`\`\`typescript\n// BAD\nconst apiKey = 'sk-abc123...'\n\n// GOOD\nconst apiKey = process.env.API_KEY\n\`\`\`\n`
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
case 'missing_auth':
|
|
54
|
+
md += `**DO NOT** expose endpoints without authentication.\n\n`
|
|
55
|
+
md += `\`\`\`typescript\n// BAD\napp.get('/api/data', handler)\n\n// GOOD\napp.get('/api/data', authMiddleware, handler)\n\`\`\`\n`
|
|
56
|
+
break
|
|
57
|
+
|
|
58
|
+
case 'dangerous_function':
|
|
59
|
+
md += `**DO NOT** use eval() or similar dangerous functions with user input.\n\n`
|
|
60
|
+
md += `\`\`\`typescript\n// BAD\neval(userInput)\n\n// GOOD\nJSON.parse(userInput)\n\`\`\`\n`
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
default:
|
|
64
|
+
// Generic fix pattern
|
|
65
|
+
if (vuln.fixSteps && vuln.fixSteps.length > 0) {
|
|
66
|
+
md += `**FIX:**\n`
|
|
67
|
+
vuln.fixSteps.forEach((step, i) => {
|
|
68
|
+
md += `${i + 1}. ${step}\n`
|
|
69
|
+
})
|
|
70
|
+
} else if (vuln.suggestedFix) {
|
|
71
|
+
md += `**FIX:** ${vuln.suggestedFix}\n`
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return md
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Format scan result as Cursor MDC rules
|
|
80
|
+
*
|
|
81
|
+
* @param result - The scan result to format
|
|
82
|
+
* @returns MDC format string for .cursor/rules/security.mdc
|
|
83
|
+
*/
|
|
84
|
+
export function formatCursorRules(result: ScanResult): string {
|
|
85
|
+
const { vulnerabilities, timestamp } = result
|
|
86
|
+
|
|
87
|
+
// Sort by severity
|
|
88
|
+
const sorted = sortBySeverity(vulnerabilities)
|
|
89
|
+
|
|
90
|
+
// Extract glob patterns
|
|
91
|
+
const globs = extractGlobPatterns(sorted)
|
|
92
|
+
|
|
93
|
+
let md = ''
|
|
94
|
+
|
|
95
|
+
// MDC Frontmatter
|
|
96
|
+
md += `---\n`
|
|
97
|
+
md += `description: Oculum Security Findings\n`
|
|
98
|
+
if (globs.length > 0) {
|
|
99
|
+
md += `globs: "${globs.join(',')}"\n`
|
|
100
|
+
}
|
|
101
|
+
md += `alwaysApply: false\n`
|
|
102
|
+
md += `---\n\n`
|
|
103
|
+
|
|
104
|
+
// No findings case
|
|
105
|
+
if (vulnerabilities.length === 0) {
|
|
106
|
+
md += `# Security Scan Results\n\n`
|
|
107
|
+
md += `No security issues found. Last scan: ${timestamp}\n\n`
|
|
108
|
+
md += `*Run \`oculum scan --cursor --clear\` to remove this file.*\n`
|
|
109
|
+
return md
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Header
|
|
113
|
+
md += `# Security Issues (${vulnerabilities.length})\n\n`
|
|
114
|
+
md += `> Last scan: ${timestamp}\n\n`
|
|
115
|
+
|
|
116
|
+
// Group by severity
|
|
117
|
+
const bySeverity = new Map<string, Vulnerability[]>()
|
|
118
|
+
for (const vuln of sorted) {
|
|
119
|
+
const group = bySeverity.get(vuln.severity) || []
|
|
120
|
+
group.push(vuln)
|
|
121
|
+
bySeverity.set(vuln.severity, group)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Output by severity
|
|
125
|
+
for (const [severity, vulns] of bySeverity) {
|
|
126
|
+
md += `## ${severity.toUpperCase()} (${vulns.length})\n\n`
|
|
127
|
+
|
|
128
|
+
for (const vuln of vulns.slice(0, 10)) {
|
|
129
|
+
md += `### ${vuln.title}\n\n`
|
|
130
|
+
md += `**File:** \`${vuln.filePath}:${vuln.lineNumber}\`\n\n`
|
|
131
|
+
|
|
132
|
+
// Add fix pattern
|
|
133
|
+
md += formatFixPattern(vuln)
|
|
134
|
+
md += '\n'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (vulns.length > 10) {
|
|
138
|
+
md += `> + ${vulns.length - 10} more ${severity} issues\n\n`
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Footer
|
|
143
|
+
md += `---\n\n`
|
|
144
|
+
md += `*Run \`oculum scan --cursor --clear\` after fixing to remove this file.*\n`
|
|
145
|
+
|
|
146
|
+
return md
|
|
147
|
+
}
|