solidity-argus 0.1.8 → 0.3.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/AGENTS.md +3 -3
- package/README.md +229 -13
- package/package.json +37 -8
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +72 -6
- package/skills/case-studies/beanstalk-governance/SKILL.md +52 -0
- package/skills/case-studies/bzx-flash-loan/SKILL.md +53 -0
- package/skills/case-studies/cream-finance/SKILL.md +52 -0
- package/skills/case-studies/curve-reentrancy/SKILL.md +52 -0
- package/skills/case-studies/dao-hack/SKILL.md +51 -0
- package/skills/case-studies/euler-finance/SKILL.md +52 -0
- package/skills/case-studies/harvest-finance/SKILL.md +52 -0
- package/skills/case-studies/level-finance/SKILL.md +51 -0
- package/skills/case-studies/mango-markets/SKILL.md +53 -0
- package/skills/case-studies/nomad-bridge/SKILL.md +51 -0
- package/skills/case-studies/parity-multisig/SKILL.md +55 -0
- package/skills/case-studies/poly-network/SKILL.md +51 -0
- package/skills/case-studies/rari-fuse/SKILL.md +51 -0
- package/skills/case-studies/ronin-bridge/SKILL.md +52 -0
- package/skills/case-studies/wormhole-bridge/SKILL.md +51 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +3 -0
- package/skills/manifests/cyfrin.json +16 -0
- package/skills/manifests/defifofum.json +25 -0
- package/skills/manifests/kadenzipfel.json +48 -0
- package/skills/manifests/scvd.json +9 -0
- package/skills/manifests/smartbugs.json +9 -0
- package/skills/manifests/solodit.json +9 -0
- package/skills/manifests/sunweb3sec.json +9 -0
- package/skills/manifests/trailofbits.json +9 -0
- package/skills/methodology/audit-workflow/SKILL.md +3 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +3 -0
- package/skills/references/exploit-reference/SKILL.md +3 -0
- package/skills/vulnerability-patterns/access-control/SKILL.md +27 -0
- package/skills/vulnerability-patterns/arbitrary-storage-location/SKILL.md +13 -1
- package/skills/vulnerability-patterns/assert-violation/SKILL.md +8 -1
- package/skills/vulnerability-patterns/asserting-contract-from-code-size/SKILL.md +12 -1
- package/skills/vulnerability-patterns/authorization-txorigin/SKILL.md +8 -1
- package/skills/vulnerability-patterns/cross-chain-bridge-vulnerabilities/SKILL.md +217 -0
- package/skills/vulnerability-patterns/default-visibility/SKILL.md +13 -1
- package/skills/vulnerability-patterns/delegatecall-untrusted-callee/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +14 -1
- package/skills/vulnerability-patterns/erc4626-exchange-rate-manipulation/SKILL.md +64 -0
- package/skills/vulnerability-patterns/fee-on-transfer-tokens/SKILL.md +93 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +13 -0
- package/skills/vulnerability-patterns/floating-pragma/SKILL.md +8 -1
- package/skills/vulnerability-patterns/front-running-attacks/SKILL.md +209 -0
- package/skills/vulnerability-patterns/gas-optimization-patterns/SKILL.md +203 -0
- package/skills/vulnerability-patterns/governance-attacks/SKILL.md +208 -0
- package/skills/vulnerability-patterns/hash-collision/SKILL.md +8 -1
- package/skills/vulnerability-patterns/inadherence-to-standards/SKILL.md +12 -1
- package/skills/vulnerability-patterns/incorrect-constructor/SKILL.md +8 -1
- package/skills/vulnerability-patterns/incorrect-inheritance-order/SKILL.md +8 -1
- package/skills/vulnerability-patterns/insufficient-gas-griefing/SKILL.md +12 -1
- package/skills/vulnerability-patterns/lack-of-precision/SKILL.md +7 -1
- package/skills/vulnerability-patterns/logic-errors/SKILL.md +10 -0
- package/skills/vulnerability-patterns/missing-parameter-bounds/SKILL.md +44 -0
- package/skills/vulnerability-patterns/missing-protection-signature-replay/SKILL.md +17 -1
- package/skills/vulnerability-patterns/msgvalue-loop/SKILL.md +12 -1
- package/skills/vulnerability-patterns/off-by-one/SKILL.md +7 -1
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +22 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +11 -1
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +22 -0
- package/skills/vulnerability-patterns/shadowing-state-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/share-accounting-desynchronization/SKILL.md +44 -0
- package/skills/vulnerability-patterns/signature-malleability/SKILL.md +11 -1
- package/skills/vulnerability-patterns/stateful-parameter-update-drift/SKILL.md +44 -0
- package/skills/vulnerability-patterns/unbounded-return-data/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unchecked-return-values/SKILL.md +13 -1
- package/skills/vulnerability-patterns/unencrypted-private-data-on-chain/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unexpected-ecrecover-null-address/SKILL.md +8 -1
- package/skills/vulnerability-patterns/uninitialized-storage-pointer/SKILL.md +8 -1
- package/skills/vulnerability-patterns/unsafe-erc20-transfers/SKILL.md +132 -0
- package/skills/vulnerability-patterns/unsafe-low-level-call/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsecure-signatures/SKILL.md +12 -1
- package/skills/vulnerability-patterns/unsupported-opcodes/SKILL.md +11 -1
- package/skills/vulnerability-patterns/unused-variables/SKILL.md +8 -1
- package/skills/vulnerability-patterns/use-of-deprecated-functions/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weak-sources-randomness/SKILL.md +8 -1
- package/skills/vulnerability-patterns/weird-tokens/SKILL.md +10 -0
- package/skills/vulnerability-patterns/zero-address-misconfiguration/SKILL.md +48 -0
- package/src/agents/argus-prompt.ts +27 -10
- package/src/agents/pythia-prompt.ts +7 -8
- package/src/agents/scribe-prompt.ts +10 -5
- package/src/agents/sentinel-prompt.ts +36 -7
- package/src/cli/cli-output.ts +16 -0
- package/src/cli/cli-program.ts +29 -22
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +303 -23
- package/src/cli/commands/init.ts +8 -6
- package/src/cli/commands/install.ts +10 -8
- package/src/cli/commands/lint-skills.ts +118 -0
- package/src/cli/index.ts +5 -5
- package/src/cli/tui-prompts.ts +4 -2
- package/src/cli/types.ts +3 -3
- package/src/config/index.ts +1 -1
- package/src/config/loader.ts +4 -6
- package/src/config/schema.ts +6 -5
- package/src/config/types.ts +2 -2
- package/src/constants/defaults.ts +2 -0
- package/src/create-hooks.ts +225 -29
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +14 -8
- package/src/features/background-agent/background-manager.ts +93 -87
- package/src/features/background-agent/index.ts +1 -1
- package/src/features/context-monitor/context-monitor.ts +3 -3
- package/src/features/context-monitor/index.ts +2 -2
- package/src/features/error-recovery/session-recovery.ts +2 -4
- package/src/features/error-recovery/tool-error-recovery.ts +79 -19
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +158 -52
- package/src/features/persistent-state/global-run-index.ts +38 -0
- package/src/features/persistent-state/index.ts +1 -1
- package/src/features/persistent-state/run-journal.ts +86 -0
- package/src/hooks/agent-tracker.ts +53 -0
- package/src/hooks/compaction-hook.ts +46 -37
- package/src/hooks/config-handler.ts +31 -11
- package/src/hooks/context-budget.ts +42 -0
- package/src/hooks/event-hook.ts +48 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +19 -21
- package/src/hooks/recon-context-builder.ts +66 -0
- package/src/hooks/safe-create-hook.ts +9 -11
- package/src/hooks/system-prompt-hook.ts +128 -0
- package/src/hooks/tool-tracking-hook.ts +162 -29
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -13
- package/src/knowledge/retry.ts +53 -0
- package/src/knowledge/scvd-client.ts +103 -83
- package/src/knowledge/scvd-errors.ts +89 -0
- package/src/knowledge/scvd-index.ts +110 -62
- package/src/knowledge/scvd-sync.ts +223 -47
- package/src/knowledge/source-manifest.ts +102 -0
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +19 -8
- package/src/shared/binary-utils.ts +44 -34
- package/src/shared/deep-merge.ts +55 -36
- package/src/shared/file-utils.ts +21 -19
- package/src/shared/index.ts +11 -5
- package/src/shared/jsonc-parser.ts +123 -28
- package/src/shared/logger.ts +91 -17
- package/src/shared/project-utils.ts +30 -0
- package/src/skills/analysis/cluster.ts +414 -0
- package/src/skills/analysis/gates.ts +227 -0
- package/src/skills/analysis/index.ts +33 -0
- package/src/skills/analysis/normalize.ts +217 -0
- package/src/skills/analysis/similarity.ts +224 -0
- package/src/skills/argus-skill-resolver.ts +237 -0
- package/src/skills/skill-schema.ts +99 -0
- package/src/solodit-lifecycle.ts +202 -0
- package/src/state/audit-state.ts +10 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +96 -44
- package/src/tools/argus-skill-load-tool.ts +78 -0
- package/src/tools/contract-analyzer-tool.ts +60 -77
- package/src/tools/forge-coverage-tool.ts +226 -0
- package/src/tools/forge-fuzz-tool.ts +127 -127
- package/src/tools/forge-test-tool.ts +153 -157
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +206 -167
- package/src/tools/pattern-loader.ts +77 -0
- package/src/tools/pattern-schema.ts +51 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +333 -142
- package/src/tools/slither-tool.ts +300 -210
- package/src/tools/solodit-search-tool.ts +255 -80
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +118 -0
- package/src/utils/dependency-scanner.ts +93 -0
- package/src/utils/project-detector.ts +175 -86
- package/src/utils/solidity-parser.ts +112 -67
- package/src/utils/solodit-health.ts +29 -0
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
|
@@ -1,79 +1,79 @@
|
|
|
1
1
|
export interface ScvdFinding {
|
|
2
|
-
scvd_id: string
|
|
3
|
-
doc_id: string
|
|
4
|
-
title: string
|
|
5
|
-
description_md: string
|
|
6
|
-
severity: "Critical" | "High" | "Medium" | "Low" | "Informational"
|
|
7
|
-
taxonomy: { swc: string[]; cwe: string[] }
|
|
8
|
-
repo: { url: string; commit?: string; lines?: [number, number] }
|
|
9
|
-
sections: { recommendation_md?: string; poc_md?: string }
|
|
2
|
+
scvd_id: string
|
|
3
|
+
doc_id: string
|
|
4
|
+
title: string
|
|
5
|
+
description_md: string
|
|
6
|
+
severity: "Critical" | "High" | "Medium" | "Low" | "Informational"
|
|
7
|
+
taxonomy: { swc: string[]; cwe: string[] }
|
|
8
|
+
repo: { url: string; commit?: string; lines?: [number, number] }
|
|
9
|
+
sections: { recommendation_md?: string; poc_md?: string }
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export interface ScvdStats {
|
|
13
|
-
total: number
|
|
14
|
-
by_severity: Record<string, number
|
|
15
|
-
last_updated: string
|
|
13
|
+
total: number
|
|
14
|
+
by_severity: Record<string, number>
|
|
15
|
+
last_updated: string
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const DEFAULT_PAGE_SIZE = 100
|
|
18
|
+
const DEFAULT_PAGE_SIZE = 100
|
|
19
19
|
|
|
20
20
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
21
|
-
return typeof value === "object" && value !== null
|
|
21
|
+
return typeof value === "object" && value !== null
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function toStringArray(value: unknown): string[] {
|
|
25
25
|
if (!Array.isArray(value)) {
|
|
26
|
-
return []
|
|
26
|
+
return []
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
return value.filter((item): item is string => typeof item === "string")
|
|
29
|
+
return value.filter((item): item is string => typeof item === "string")
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function toNumberRecord(value: unknown): Record<string, number> {
|
|
33
33
|
if (!isRecord(value)) {
|
|
34
|
-
return {}
|
|
34
|
+
return {}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const output: Record<string, number> = {}
|
|
37
|
+
const output: Record<string, number> = {}
|
|
38
38
|
for (const [key, rawValue] of Object.entries(value)) {
|
|
39
39
|
if (typeof rawValue === "number" && Number.isFinite(rawValue)) {
|
|
40
|
-
output[key] = rawValue
|
|
40
|
+
output[key] = rawValue
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
return output
|
|
44
|
+
return output
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function parseLines(value: unknown): [number, number] | undefined {
|
|
48
48
|
if (!Array.isArray(value) || value.length !== 2) {
|
|
49
|
-
return undefined
|
|
49
|
+
return undefined
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const start = value[0]
|
|
53
|
-
const end = value[1]
|
|
52
|
+
const start = value[0]
|
|
53
|
+
const end = value[1]
|
|
54
54
|
|
|
55
55
|
if (typeof start !== "number" || typeof end !== "number") {
|
|
56
|
-
return undefined
|
|
56
|
+
return undefined
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
return [start, end]
|
|
59
|
+
return [start, end]
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function parseFinding(raw: unknown): ScvdFinding | null {
|
|
63
63
|
if (!isRecord(raw)) {
|
|
64
|
-
return null
|
|
64
|
+
return null
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const taxonomyRaw = isRecord(raw.taxonomy) ? raw.taxonomy : {}
|
|
68
|
-
const repoRaw = isRecord(raw.repo) ? raw.repo : {}
|
|
69
|
-
const sectionsRaw = isRecord(raw.sections) ? raw.sections : {}
|
|
67
|
+
const taxonomyRaw = isRecord(raw.taxonomy) ? raw.taxonomy : {}
|
|
68
|
+
const repoRaw = isRecord(raw.repo) ? raw.repo : {}
|
|
69
|
+
const sectionsRaw = isRecord(raw.sections) ? raw.sections : {}
|
|
70
70
|
|
|
71
|
-
const scvdId = raw.scvd_id
|
|
72
|
-
const docId = raw.doc_id
|
|
73
|
-
const title = raw.title
|
|
74
|
-
const description = raw.description_md
|
|
75
|
-
const severity = raw.severity
|
|
76
|
-
const repoUrl = repoRaw.url
|
|
71
|
+
const scvdId = raw.scvd_id
|
|
72
|
+
const docId = raw.doc_id
|
|
73
|
+
const title = raw.title
|
|
74
|
+
const description = raw.description_md
|
|
75
|
+
const severity = raw.severity
|
|
76
|
+
const repoUrl = repoRaw.url
|
|
77
77
|
|
|
78
78
|
if (
|
|
79
79
|
typeof scvdId !== "string" ||
|
|
@@ -82,7 +82,7 @@ function parseFinding(raw: unknown): ScvdFinding | null {
|
|
|
82
82
|
typeof description !== "string" ||
|
|
83
83
|
typeof repoUrl !== "string"
|
|
84
84
|
) {
|
|
85
|
-
return null
|
|
85
|
+
return null
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (
|
|
@@ -92,7 +92,7 @@ function parseFinding(raw: unknown): ScvdFinding | null {
|
|
|
92
92
|
severity !== "Low" &&
|
|
93
93
|
severity !== "Informational"
|
|
94
94
|
) {
|
|
95
|
-
return null
|
|
95
|
+
return null
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
return {
|
|
@@ -117,126 +117,146 @@ function parseFinding(raw: unknown): ScvdFinding | null {
|
|
|
117
117
|
: undefined,
|
|
118
118
|
poc_md: typeof sectionsRaw.poc_md === "string" ? sectionsRaw.poc_md : undefined,
|
|
119
119
|
},
|
|
120
|
-
}
|
|
120
|
+
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function parseFindings(raw: unknown): ScvdFinding[] {
|
|
124
124
|
if (!Array.isArray(raw)) {
|
|
125
125
|
if (isRecord(raw) && Array.isArray(raw.data)) {
|
|
126
|
-
return raw.data.map(parseFinding).filter((value): value is ScvdFinding => value !== null)
|
|
126
|
+
return raw.data.map(parseFinding).filter((value): value is ScvdFinding => value !== null)
|
|
127
127
|
}
|
|
128
|
-
return []
|
|
128
|
+
return []
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
return raw.map(parseFinding).filter((value): value is ScvdFinding => value !== null)
|
|
131
|
+
return raw.map(parseFinding).filter((value): value is ScvdFinding => value !== null)
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function parseStats(raw: unknown): ScvdStats {
|
|
135
135
|
if (!isRecord(raw)) {
|
|
136
|
-
throw new Error("Invalid SCVD stats response payload")
|
|
136
|
+
throw new Error("Invalid SCVD stats response payload")
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const total = raw.total
|
|
140
|
-
const lastUpdated = raw.last_updated
|
|
139
|
+
const total = raw.total
|
|
140
|
+
const lastUpdated = raw.last_updated
|
|
141
141
|
|
|
142
142
|
if (typeof total !== "number" || typeof lastUpdated !== "string") {
|
|
143
|
-
throw new Error("Invalid SCVD stats fields in response")
|
|
143
|
+
throw new Error("Invalid SCVD stats fields in response")
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
return {
|
|
147
147
|
total,
|
|
148
148
|
by_severity: toNumberRecord(raw.by_severity),
|
|
149
149
|
last_updated: lastUpdated,
|
|
150
|
-
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class ScvdNetworkError extends Error {
|
|
154
|
+
override readonly name = "ScvdNetworkError" as const
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class ScvdApiError extends Error {
|
|
158
|
+
override readonly name = "ScvdApiError" as const
|
|
159
|
+
readonly httpStatus: number
|
|
160
|
+
|
|
161
|
+
constructor(httpStatus: number, message?: string) {
|
|
162
|
+
super(message ?? `SCVD API error: HTTP ${httpStatus}`)
|
|
163
|
+
this.httpStatus = httpStatus
|
|
164
|
+
}
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
export class ScvdClient {
|
|
154
|
-
private readonly baseUrl: string
|
|
155
|
-
private readonly signal?: AbortSignal
|
|
168
|
+
private readonly baseUrl: string
|
|
169
|
+
private readonly signal?: AbortSignal
|
|
156
170
|
|
|
157
171
|
constructor(apiUrl: string, signal?: AbortSignal) {
|
|
158
|
-
this.baseUrl = apiUrl.replace(/\/$/, "")
|
|
159
|
-
this.signal = signal
|
|
172
|
+
this.baseUrl = apiUrl.replace(/\/$/, "")
|
|
173
|
+
this.signal = signal
|
|
160
174
|
}
|
|
161
175
|
|
|
162
176
|
async fetchStats(): Promise<ScvdStats> {
|
|
163
|
-
const url = `${this.baseUrl}/stats
|
|
177
|
+
const url = `${this.baseUrl}/stats`
|
|
164
178
|
|
|
165
|
-
let response: Response
|
|
179
|
+
let response: Response
|
|
166
180
|
try {
|
|
167
|
-
response = await fetch(url, { signal: this.signal })
|
|
181
|
+
response = await fetch(url, { signal: this.signal })
|
|
168
182
|
} catch (error) {
|
|
169
|
-
const message = error instanceof Error ? error.message : "unknown network error"
|
|
170
|
-
throw new
|
|
183
|
+
const message = error instanceof Error ? error.message : "unknown network error"
|
|
184
|
+
throw new ScvdNetworkError(`Failed to fetch SCVD stats from ${url}: ${message}`)
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
if (!response.ok) {
|
|
174
|
-
throw new
|
|
188
|
+
throw new ScvdApiError(
|
|
189
|
+
response.status,
|
|
190
|
+
`Failed to fetch SCVD stats from ${url}: HTTP ${response.status}`,
|
|
191
|
+
)
|
|
175
192
|
}
|
|
176
193
|
|
|
177
|
-
const body = (await response.json()) as unknown
|
|
178
|
-
return parseStats(body)
|
|
194
|
+
const body = (await response.json()) as unknown
|
|
195
|
+
return parseStats(body)
|
|
179
196
|
}
|
|
180
197
|
|
|
181
198
|
async fetchFindings(params: {
|
|
182
|
-
severity?: string
|
|
183
|
-
limit?: number
|
|
184
|
-
offset?: number
|
|
199
|
+
severity?: string
|
|
200
|
+
limit?: number
|
|
201
|
+
offset?: number
|
|
185
202
|
}): Promise<ScvdFinding[]> {
|
|
186
|
-
const searchParams = new URLSearchParams()
|
|
203
|
+
const searchParams = new URLSearchParams()
|
|
187
204
|
|
|
188
205
|
if (params.severity) {
|
|
189
|
-
searchParams.set("severity", params.severity)
|
|
206
|
+
searchParams.set("severity", params.severity)
|
|
190
207
|
}
|
|
191
208
|
if (typeof params.limit === "number") {
|
|
192
|
-
searchParams.set("limit", String(params.limit))
|
|
209
|
+
searchParams.set("limit", String(params.limit))
|
|
193
210
|
}
|
|
194
211
|
if (typeof params.offset === "number") {
|
|
195
|
-
searchParams.set("offset", String(params.offset))
|
|
212
|
+
searchParams.set("offset", String(params.offset))
|
|
196
213
|
}
|
|
197
214
|
|
|
198
|
-
const query = searchParams.toString()
|
|
199
|
-
const url = `${this.baseUrl}/findings${query.length > 0 ? `?${query}` : ""}
|
|
215
|
+
const query = searchParams.toString()
|
|
216
|
+
const url = `${this.baseUrl}/findings${query.length > 0 ? `?${query}` : ""}`
|
|
200
217
|
|
|
218
|
+
let response: Response
|
|
201
219
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
220
|
+
response = await fetch(url, { signal: this.signal })
|
|
221
|
+
} catch (error) {
|
|
222
|
+
const message = error instanceof Error ? error.message : "unknown network error"
|
|
223
|
+
throw new ScvdNetworkError(`Failed to fetch SCVD findings from ${url}: ${message}`)
|
|
224
|
+
}
|
|
206
225
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
} catch {
|
|
210
|
-
return []; // network error — treat as empty page
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
throw new ScvdApiError(response.status, `SCVD API error: HTTP ${response.status} for ${url}`)
|
|
211
228
|
}
|
|
229
|
+
|
|
230
|
+
const body = (await response.json()) as unknown
|
|
231
|
+
return parseFindings(body)
|
|
212
232
|
}
|
|
213
233
|
|
|
214
234
|
async fetchAllFindings(onProgress?: (count: number) => void): Promise<ScvdFinding[]> {
|
|
215
|
-
const results: ScvdFinding[] = []
|
|
216
|
-
let offset = 0
|
|
235
|
+
const results: ScvdFinding[] = []
|
|
236
|
+
let offset = 0
|
|
217
237
|
|
|
218
238
|
while (true) {
|
|
219
239
|
const page = await this.fetchFindings({
|
|
220
240
|
limit: DEFAULT_PAGE_SIZE,
|
|
221
241
|
offset,
|
|
222
|
-
})
|
|
242
|
+
})
|
|
223
243
|
|
|
224
244
|
if (page.length === 0) {
|
|
225
|
-
break
|
|
245
|
+
break
|
|
226
246
|
}
|
|
227
247
|
|
|
228
|
-
results.push(...page)
|
|
229
|
-
offset += page.length
|
|
248
|
+
results.push(...page)
|
|
249
|
+
offset += page.length
|
|
230
250
|
|
|
231
251
|
if (onProgress) {
|
|
232
|
-
onProgress(results.length)
|
|
252
|
+
onProgress(results.length)
|
|
233
253
|
}
|
|
234
254
|
|
|
235
255
|
if (page.length < DEFAULT_PAGE_SIZE) {
|
|
236
|
-
break
|
|
256
|
+
break
|
|
237
257
|
}
|
|
238
258
|
}
|
|
239
259
|
|
|
240
|
-
return results
|
|
260
|
+
return results
|
|
241
261
|
}
|
|
242
262
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
export type SyncError = {
|
|
2
|
+
status: "error"
|
|
3
|
+
success: false
|
|
4
|
+
reason: "network" | "api" | "parse"
|
|
5
|
+
message: string
|
|
6
|
+
error: string
|
|
7
|
+
httpStatus?: number
|
|
8
|
+
newFindings: 0
|
|
9
|
+
totalIndexed: 0
|
|
10
|
+
lastSync: string
|
|
11
|
+
attempts?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SyncSuccess = {
|
|
15
|
+
status: "success"
|
|
16
|
+
success: true
|
|
17
|
+
newFindings: number
|
|
18
|
+
totalIndexed: number
|
|
19
|
+
lastSync: string
|
|
20
|
+
error?: undefined
|
|
21
|
+
attempts?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type SyncStale = {
|
|
25
|
+
status: "stale"
|
|
26
|
+
success: false
|
|
27
|
+
newFindings: 0
|
|
28
|
+
totalIndexed: 0
|
|
29
|
+
lastSync: string
|
|
30
|
+
error?: undefined
|
|
31
|
+
daysSinceSync: number
|
|
32
|
+
attempts?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type SyncOutcome = SyncSuccess | SyncError | SyncStale
|
|
36
|
+
|
|
37
|
+
export function createNetworkError(message: string): SyncError {
|
|
38
|
+
return {
|
|
39
|
+
status: "error",
|
|
40
|
+
success: false,
|
|
41
|
+
reason: "network",
|
|
42
|
+
message,
|
|
43
|
+
error: message,
|
|
44
|
+
newFindings: 0,
|
|
45
|
+
totalIndexed: 0,
|
|
46
|
+
lastSync: new Date().toISOString(),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createApiError(httpStatus: number, message: string): SyncError {
|
|
51
|
+
return {
|
|
52
|
+
status: "error",
|
|
53
|
+
success: false,
|
|
54
|
+
reason: "api",
|
|
55
|
+
message,
|
|
56
|
+
error: message,
|
|
57
|
+
httpStatus,
|
|
58
|
+
newFindings: 0,
|
|
59
|
+
totalIndexed: 0,
|
|
60
|
+
lastSync: new Date().toISOString(),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createParseError(message: string): SyncError {
|
|
65
|
+
return {
|
|
66
|
+
status: "error",
|
|
67
|
+
success: false,
|
|
68
|
+
reason: "parse",
|
|
69
|
+
message,
|
|
70
|
+
error: message,
|
|
71
|
+
newFindings: 0,
|
|
72
|
+
totalIndexed: 0,
|
|
73
|
+
lastSync: new Date().toISOString(),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createSyncSuccess(
|
|
78
|
+
data: Omit<SyncSuccess, "status" | "success" | "error"> & { attempts?: number },
|
|
79
|
+
): SyncSuccess {
|
|
80
|
+
return {
|
|
81
|
+
status: "success",
|
|
82
|
+
success: true,
|
|
83
|
+
...data,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isRetryableError(outcome: SyncOutcome): boolean {
|
|
88
|
+
return outcome.status === "error" && outcome.reason === "network"
|
|
89
|
+
}
|
|
@@ -1,39 +1,66 @@
|
|
|
1
|
-
import type { ScvdFinding } from "./scvd-client"
|
|
1
|
+
import type { ScvdFinding } from "./scvd-client"
|
|
2
2
|
|
|
3
3
|
export interface ScvdIndexEntry {
|
|
4
|
-
id: string
|
|
5
|
-
title: string
|
|
6
|
-
severity: string
|
|
7
|
-
swc: string[]
|
|
8
|
-
cwe: string[]
|
|
9
|
-
keywords: string[]
|
|
10
|
-
repoUrl: string
|
|
4
|
+
id: string
|
|
5
|
+
title: string
|
|
6
|
+
severity: string
|
|
7
|
+
swc: string[]
|
|
8
|
+
cwe: string[]
|
|
9
|
+
keywords: string[]
|
|
10
|
+
repoUrl: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ScvdIndexMetadata {
|
|
14
|
+
lastSuccess: string | null
|
|
15
|
+
lastAttempt: string | null
|
|
16
|
+
errorCount: number
|
|
17
|
+
lastError: string | null
|
|
18
|
+
lastErrorReason: string | null
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
export interface ScvdIndex {
|
|
14
|
-
version: number
|
|
15
|
-
lastSync: string
|
|
16
|
-
totalFindings: number
|
|
17
|
-
entries: ScvdIndexEntry[]
|
|
22
|
+
version: number
|
|
23
|
+
lastSync: string
|
|
24
|
+
totalFindings: number
|
|
25
|
+
entries: ScvdIndexEntry[]
|
|
26
|
+
metadata?: ScvdIndexMetadata
|
|
18
27
|
}
|
|
19
28
|
|
|
20
|
-
const INDEX_VERSION = 1
|
|
21
|
-
const DEFAULT_LIMIT = 10
|
|
29
|
+
const INDEX_VERSION = 1
|
|
30
|
+
const DEFAULT_LIMIT = 10
|
|
31
|
+
let syncInProgress = false
|
|
32
|
+
|
|
33
|
+
export function acquireSyncLock(): boolean {
|
|
34
|
+
if (syncInProgress) {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
syncInProgress = true
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function releaseSyncLock(): void {
|
|
43
|
+
syncInProgress = false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isSyncLocked(): boolean {
|
|
47
|
+
return syncInProgress
|
|
48
|
+
}
|
|
22
49
|
|
|
23
50
|
function normalizeKeywordInput(value: string): string[] {
|
|
24
51
|
return value
|
|
25
52
|
.toLowerCase()
|
|
26
53
|
.split(/[^a-z0-9]+/g)
|
|
27
54
|
.map((word) => word.trim())
|
|
28
|
-
.filter((word) => word.length > 1)
|
|
55
|
+
.filter((word) => word.length > 1)
|
|
29
56
|
}
|
|
30
57
|
|
|
31
58
|
function uniqueWords(words: string[]): string[] {
|
|
32
|
-
return Array.from(new Set(words))
|
|
59
|
+
return Array.from(new Set(words))
|
|
33
60
|
}
|
|
34
61
|
|
|
35
62
|
function findingToEntry(finding: ScvdFinding): ScvdIndexEntry {
|
|
36
|
-
const keywordSource = `${finding.title} ${finding.description_md}
|
|
63
|
+
const keywordSource = `${finding.title} ${finding.description_md}`
|
|
37
64
|
|
|
38
65
|
return {
|
|
39
66
|
id: finding.scvd_id,
|
|
@@ -43,84 +70,84 @@ function findingToEntry(finding: ScvdFinding): ScvdIndexEntry {
|
|
|
43
70
|
cwe: finding.taxonomy.cwe,
|
|
44
71
|
keywords: uniqueWords(normalizeKeywordInput(keywordSource)),
|
|
45
72
|
repoUrl: finding.repo.url,
|
|
46
|
-
}
|
|
73
|
+
}
|
|
47
74
|
}
|
|
48
75
|
|
|
49
76
|
export function buildIndex(findings: ScvdFinding[]): ScvdIndex {
|
|
50
|
-
const now = new Date().toISOString()
|
|
51
|
-
const entries = findings.map(findingToEntry)
|
|
77
|
+
const now = new Date().toISOString()
|
|
78
|
+
const entries = findings.map(findingToEntry)
|
|
52
79
|
|
|
53
80
|
return {
|
|
54
81
|
version: INDEX_VERSION,
|
|
55
82
|
lastSync: now,
|
|
56
83
|
totalFindings: entries.length,
|
|
57
84
|
entries,
|
|
58
|
-
}
|
|
85
|
+
}
|
|
59
86
|
}
|
|
60
87
|
|
|
61
88
|
export function searchIndex(
|
|
62
89
|
index: ScvdIndex,
|
|
63
90
|
query: {
|
|
64
|
-
swc?: string
|
|
65
|
-
severity?: string
|
|
66
|
-
keyword?: string
|
|
67
|
-
limit?: number
|
|
68
|
-
}
|
|
91
|
+
swc?: string
|
|
92
|
+
severity?: string
|
|
93
|
+
keyword?: string
|
|
94
|
+
limit?: number
|
|
95
|
+
},
|
|
69
96
|
): ScvdIndexEntry[] {
|
|
70
|
-
const normalizedKeyword = query.keyword?.toLowerCase().trim()
|
|
71
|
-
const limit = query.limit ?? DEFAULT_LIMIT
|
|
97
|
+
const normalizedKeyword = query.keyword?.toLowerCase().trim()
|
|
98
|
+
const limit = query.limit ?? DEFAULT_LIMIT
|
|
72
99
|
|
|
73
100
|
const filtered = index.entries.filter((entry) => {
|
|
74
101
|
if (query.swc && !entry.swc.includes(query.swc)) {
|
|
75
|
-
return false
|
|
102
|
+
return false
|
|
76
103
|
}
|
|
77
104
|
|
|
78
105
|
if (query.severity && entry.severity !== query.severity) {
|
|
79
|
-
return false
|
|
106
|
+
return false
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
if (normalizedKeyword && normalizedKeyword.length > 0) {
|
|
83
|
-
const matchesKeyword = entry.keywords.some((keyword) =>
|
|
84
|
-
keyword.includes(normalizedKeyword)
|
|
85
|
-
);
|
|
110
|
+
const matchesKeyword = entry.keywords.some((keyword) => keyword.includes(normalizedKeyword))
|
|
86
111
|
|
|
87
112
|
if (!matchesKeyword) {
|
|
88
|
-
return false
|
|
113
|
+
return false
|
|
89
114
|
}
|
|
90
115
|
}
|
|
91
116
|
|
|
92
|
-
return true
|
|
93
|
-
})
|
|
117
|
+
return true
|
|
118
|
+
})
|
|
94
119
|
|
|
95
|
-
return filtered.slice(0, limit)
|
|
120
|
+
return filtered.slice(0, limit)
|
|
96
121
|
}
|
|
97
122
|
|
|
98
123
|
export async function saveIndex(index: ScvdIndex, filePath: string): Promise<void> {
|
|
99
|
-
const
|
|
100
|
-
await
|
|
124
|
+
const tmpPath = `${filePath}.tmp.${Date.now()}`
|
|
125
|
+
const { renameSync } = await import("node:fs")
|
|
126
|
+
await Bun.write(tmpPath, JSON.stringify(index, null, 2))
|
|
127
|
+
renameSync(tmpPath, filePath)
|
|
101
128
|
}
|
|
102
129
|
|
|
103
130
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
104
|
-
return typeof value === "object" && value !== null
|
|
131
|
+
return typeof value === "object" && value !== null
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
function parseStringArray(value: unknown): string[] {
|
|
108
135
|
if (!Array.isArray(value)) {
|
|
109
|
-
return []
|
|
136
|
+
return []
|
|
110
137
|
}
|
|
111
138
|
|
|
112
|
-
return value.filter((item): item is string => typeof item === "string")
|
|
139
|
+
return value.filter((item): item is string => typeof item === "string")
|
|
113
140
|
}
|
|
114
141
|
|
|
115
142
|
function parseEntry(value: unknown): ScvdIndexEntry | null {
|
|
116
143
|
if (!isRecord(value)) {
|
|
117
|
-
return null
|
|
144
|
+
return null
|
|
118
145
|
}
|
|
119
146
|
|
|
120
|
-
const id = value.id
|
|
121
|
-
const title = value.title
|
|
122
|
-
const severity = value.severity
|
|
123
|
-
const repoUrl = value.repoUrl
|
|
147
|
+
const id = value.id
|
|
148
|
+
const title = value.title
|
|
149
|
+
const severity = value.severity
|
|
150
|
+
const repoUrl = value.repoUrl
|
|
124
151
|
|
|
125
152
|
if (
|
|
126
153
|
typeof id !== "string" ||
|
|
@@ -128,7 +155,7 @@ function parseEntry(value: unknown): ScvdIndexEntry | null {
|
|
|
128
155
|
typeof severity !== "string" ||
|
|
129
156
|
typeof repoUrl !== "string"
|
|
130
157
|
) {
|
|
131
|
-
return null
|
|
158
|
+
return null
|
|
132
159
|
}
|
|
133
160
|
|
|
134
161
|
return {
|
|
@@ -139,27 +166,41 @@ function parseEntry(value: unknown): ScvdIndexEntry | null {
|
|
|
139
166
|
cwe: parseStringArray(value.cwe),
|
|
140
167
|
keywords: parseStringArray(value.keywords),
|
|
141
168
|
repoUrl,
|
|
142
|
-
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseNullableString(value: unknown): string | null {
|
|
173
|
+
return typeof value === "string" ? value : null
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseMetadata(raw: Record<string, unknown>): ScvdIndexMetadata {
|
|
177
|
+
return {
|
|
178
|
+
lastSuccess: parseNullableString(raw.lastSuccess),
|
|
179
|
+
lastAttempt: parseNullableString(raw.lastAttempt),
|
|
180
|
+
errorCount: typeof raw.errorCount === "number" ? raw.errorCount : 0,
|
|
181
|
+
lastError: parseNullableString(raw.lastError),
|
|
182
|
+
lastErrorReason: parseNullableString(raw.lastErrorReason),
|
|
183
|
+
}
|
|
143
184
|
}
|
|
144
185
|
|
|
145
186
|
export async function loadIndex(filePath: string): Promise<ScvdIndex | null> {
|
|
146
|
-
const file = Bun.file(filePath)
|
|
147
|
-
const exists = await file.exists()
|
|
187
|
+
const file = Bun.file(filePath)
|
|
188
|
+
const exists = await file.exists()
|
|
148
189
|
|
|
149
190
|
if (!exists) {
|
|
150
|
-
return null
|
|
191
|
+
return null
|
|
151
192
|
}
|
|
152
193
|
|
|
153
|
-
const raw = (await file.json()) as unknown
|
|
194
|
+
const raw = (await file.json()) as unknown
|
|
154
195
|
|
|
155
196
|
if (!isRecord(raw)) {
|
|
156
|
-
return null
|
|
197
|
+
return null
|
|
157
198
|
}
|
|
158
199
|
|
|
159
|
-
const version = raw.version
|
|
160
|
-
const lastSync = raw.lastSync
|
|
161
|
-
const totalFindings = raw.totalFindings
|
|
162
|
-
const rawEntries = raw.entries
|
|
200
|
+
const version = raw.version
|
|
201
|
+
const lastSync = raw.lastSync
|
|
202
|
+
const totalFindings = raw.totalFindings
|
|
203
|
+
const rawEntries = raw.entries
|
|
163
204
|
|
|
164
205
|
if (
|
|
165
206
|
typeof version !== "number" ||
|
|
@@ -167,17 +208,24 @@ export async function loadIndex(filePath: string): Promise<ScvdIndex | null> {
|
|
|
167
208
|
typeof totalFindings !== "number" ||
|
|
168
209
|
!Array.isArray(rawEntries)
|
|
169
210
|
) {
|
|
170
|
-
return null
|
|
211
|
+
return null
|
|
171
212
|
}
|
|
172
213
|
|
|
173
214
|
const entries = rawEntries
|
|
174
215
|
.map(parseEntry)
|
|
175
|
-
.filter((entry): entry is ScvdIndexEntry => entry !== null)
|
|
216
|
+
.filter((entry): entry is ScvdIndexEntry => entry !== null)
|
|
176
217
|
|
|
177
|
-
|
|
218
|
+
const index: ScvdIndex = {
|
|
178
219
|
version,
|
|
179
220
|
lastSync,
|
|
180
221
|
totalFindings,
|
|
181
222
|
entries,
|
|
182
|
-
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const rawMetadata = raw.metadata
|
|
226
|
+
if (isRecord(rawMetadata)) {
|
|
227
|
+
index.metadata = parseMetadata(rawMetadata)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return index
|
|
183
231
|
}
|