solidity-argus 0.2.0 → 0.3.2
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 +93 -37
- package/package.json +34 -7
- package/skills/INVENTORY.md +88 -57
- package/skills/README.md +26 -23
- 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/manifests/smartbugs.json +1 -3
- package/skills/manifests/sunweb3sec.json +1 -3
- package/skills/vulnerability-patterns/access-control/SKILL.md +14 -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 +2 -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 +2 -1
- package/skills/vulnerability-patterns/dos-gas-limit/SKILL.md +8 -1
- package/skills/vulnerability-patterns/dos-revert/SKILL.md +1 -0
- 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 +1 -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 +9 -0
- package/skills/vulnerability-patterns/outdated-compiler-version/SKILL.md +8 -1
- package/skills/vulnerability-patterns/overflow-underflow/SKILL.md +1 -0
- package/skills/vulnerability-patterns/proxy-vulnerabilities/SKILL.md +209 -0
- package/skills/vulnerability-patterns/reentrancy/SKILL.md +9 -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 +2 -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 +2 -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 +34 -7
- package/src/agents/pythia-prompt.ts +13 -4
- package/src/agents/scribe-prompt.ts +20 -2
- package/src/agents/sentinel-prompt.ts +45 -5
- package/src/cli/cli-program.ts +29 -26
- package/src/cli/commands/check-skills.ts +135 -0
- package/src/cli/commands/doctor.ts +48 -26
- package/src/cli/commands/init.ts +5 -3
- package/src/cli/commands/install.ts +7 -5
- package/src/cli/commands/lint-skills.ts +16 -12
- package/src/cli/index.ts +5 -5
- 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 +145 -34
- package/src/create-managers.ts +10 -8
- package/src/create-tools.ts +13 -9
- 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 +12 -7
- package/src/features/index.ts +5 -5
- package/src/features/persistent-state/audit-state-manager.ts +143 -60
- 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/config-handler.ts +28 -11
- package/src/hooks/context-budget.ts +2 -5
- package/src/hooks/event-hook.ts +47 -23
- package/src/hooks/hook-system.ts +4 -4
- package/src/hooks/index.ts +5 -5
- package/src/hooks/knowledge-sync-hook.ts +18 -21
- package/src/hooks/recon-context-builder.ts +2 -2
- package/src/hooks/safe-create-hook.ts +6 -7
- package/src/hooks/system-prompt-hook.ts +18 -1
- package/src/hooks/tool-tracking-hook.ts +110 -51
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +24 -37
- package/src/knowledge/retry.ts +22 -22
- package/src/knowledge/scvd-client.ts +88 -95
- package/src/knowledge/scvd-errors.ts +35 -35
- package/src/knowledge/scvd-index.ts +78 -80
- package/src/knowledge/scvd-sync.ts +106 -101
- package/src/managers/index.ts +1 -1
- package/src/managers/types.ts +19 -14
- package/src/plugin-interface.ts +7 -9
- package/src/shared/binary-utils.ts +44 -35
- 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 +16 -3
- 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 +17 -6
- package/src/skills/skill-schema.ts +11 -10
- package/src/solodit-lifecycle.ts +203 -0
- package/src/state/audit-state.ts +8 -8
- package/src/state/finding-store.ts +68 -55
- package/src/state/types.ts +88 -67
- package/src/tools/argus-skill-load-tool.ts +12 -7
- package/src/tools/contract-analyzer-tool.ts +142 -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 +201 -158
- package/src/tools/gas-analysis-tool.ts +264 -0
- package/src/tools/pattern-checker-tool.ts +203 -191
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/pattern-schema.ts +3 -0
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +305 -206
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +235 -119
- package/src/tools/sync-knowledge-tool.ts +7 -11
- package/src/utils/audit-artifact-detector.ts +28 -29
- package/src/utils/dependency-scanner.ts +37 -37
- package/src/utils/project-detector.ts +111 -124
- package/src/utils/solidity-parser.ts +175 -75
- package/skills/patterns/access-control.yaml +0 -31
- package/skills/patterns/erc4626.yaml +0 -29
- package/skills/patterns/flash-loan.yaml +0 -20
- package/skills/patterns/oracle.yaml +0 -30
- package/skills/patterns/proxy.yaml +0 -30
- package/skills/patterns/reentrancy.yaml +0 -30
- package/skills/patterns/signature.yaml +0 -31
- package/src/hooks/event-hook-v2.ts +0 -99
- package/src/state/plugin-state.ts +0 -14
package/src/knowledge/retry.ts
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
1
|
export interface RetryOptions<T> {
|
|
2
|
-
maxAttempts: number
|
|
3
|
-
baseDelayMs: number
|
|
4
|
-
shouldRetry: (error: unknown) => boolean
|
|
5
|
-
onRetry?: (attempt: number, error: unknown) => void
|
|
6
|
-
_valueType?: T
|
|
2
|
+
maxAttempts: number
|
|
3
|
+
baseDelayMs: number
|
|
4
|
+
shouldRetry: (error: unknown) => boolean
|
|
5
|
+
onRetry?: (attempt: number, error: unknown) => void
|
|
6
|
+
_valueType?: T
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export interface RetryResult<T> {
|
|
10
|
-
success: boolean
|
|
11
|
-
value?: T
|
|
12
|
-
error?: unknown
|
|
13
|
-
attempts: number
|
|
10
|
+
success: boolean
|
|
11
|
+
value?: T
|
|
12
|
+
error?: unknown
|
|
13
|
+
attempts: number
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function sleep(delayMs: number): Promise<void> {
|
|
17
|
-
return new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs))
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export async function withRetry<T>(
|
|
21
21
|
fn: () => Promise<T>,
|
|
22
|
-
options: RetryOptions<T
|
|
22
|
+
options: RetryOptions<T>,
|
|
23
23
|
): Promise<RetryResult<T>> {
|
|
24
|
-
const maxAttempts = options.maxAttempts > 0 ? options.maxAttempts : 1
|
|
25
|
-
let lastError: unknown
|
|
24
|
+
const maxAttempts = options.maxAttempts > 0 ? options.maxAttempts : 1
|
|
25
|
+
let lastError: unknown
|
|
26
26
|
|
|
27
27
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
28
28
|
try {
|
|
29
|
-
const value = await fn()
|
|
30
|
-
return { success: true, value, attempts: attempt }
|
|
29
|
+
const value = await fn()
|
|
30
|
+
return { success: true, value, attempts: attempt }
|
|
31
31
|
} catch (error) {
|
|
32
|
-
lastError = error
|
|
33
|
-
const canRetry = attempt < maxAttempts && options.shouldRetry(error)
|
|
32
|
+
lastError = error
|
|
33
|
+
const canRetry = attempt < maxAttempts && options.shouldRetry(error)
|
|
34
34
|
|
|
35
35
|
if (!canRetry) {
|
|
36
|
-
return { success: false, error, attempts: attempt }
|
|
36
|
+
return { success: false, error, attempts: attempt }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
if (options.onRetry) {
|
|
40
|
-
options.onRetry(attempt, error)
|
|
40
|
+
options.onRetry(attempt, error)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const delay = options.baseDelayMs * 2 ** (attempt - 1)
|
|
44
|
-
await sleep(delay)
|
|
43
|
+
const delay = options.baseDelayMs * 2 ** (attempt - 1)
|
|
44
|
+
await sleep(delay)
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -49,5 +49,5 @@ export async function withRetry<T>(
|
|
|
49
49
|
success: false,
|
|
50
50
|
error: lastError,
|
|
51
51
|
attempts: maxAttempts,
|
|
52
|
-
}
|
|
52
|
+
}
|
|
53
53
|
}
|
|
@@ -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,153 +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
151
|
}
|
|
152
152
|
|
|
153
153
|
export class ScvdNetworkError extends Error {
|
|
154
|
-
override readonly name = "ScvdNetworkError" as const
|
|
155
|
-
|
|
156
|
-
constructor(message: string) {
|
|
157
|
-
super(message);
|
|
158
|
-
}
|
|
154
|
+
override readonly name = "ScvdNetworkError" as const
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
export class ScvdApiError extends Error {
|
|
162
|
-
override readonly name = "ScvdApiError" as const
|
|
163
|
-
readonly httpStatus: number
|
|
158
|
+
override readonly name = "ScvdApiError" as const
|
|
159
|
+
readonly httpStatus: number
|
|
164
160
|
|
|
165
161
|
constructor(httpStatus: number, message?: string) {
|
|
166
|
-
super(message ?? `SCVD API error: HTTP ${httpStatus}`)
|
|
167
|
-
this.httpStatus = httpStatus
|
|
162
|
+
super(message ?? `SCVD API error: HTTP ${httpStatus}`)
|
|
163
|
+
this.httpStatus = httpStatus
|
|
168
164
|
}
|
|
169
165
|
}
|
|
170
166
|
|
|
171
167
|
export class ScvdClient {
|
|
172
|
-
private readonly baseUrl: string
|
|
173
|
-
private readonly signal?: AbortSignal
|
|
168
|
+
private readonly baseUrl: string
|
|
169
|
+
private readonly signal?: AbortSignal
|
|
174
170
|
|
|
175
171
|
constructor(apiUrl: string, signal?: AbortSignal) {
|
|
176
|
-
this.baseUrl = apiUrl.replace(/\/$/, "")
|
|
177
|
-
this.signal = signal
|
|
172
|
+
this.baseUrl = apiUrl.replace(/\/$/, "")
|
|
173
|
+
this.signal = signal
|
|
178
174
|
}
|
|
179
175
|
|
|
180
176
|
async fetchStats(): Promise<ScvdStats> {
|
|
181
|
-
const url = `${this.baseUrl}/stats
|
|
177
|
+
const url = `${this.baseUrl}/stats`
|
|
182
178
|
|
|
183
|
-
let response: Response
|
|
179
|
+
let response: Response
|
|
184
180
|
try {
|
|
185
|
-
response = await fetch(url, { signal: this.signal })
|
|
181
|
+
response = await fetch(url, { signal: this.signal })
|
|
186
182
|
} catch (error) {
|
|
187
|
-
const message = error instanceof Error ? error.message : "unknown network error"
|
|
188
|
-
throw new ScvdNetworkError(`Failed to fetch SCVD stats from ${url}: ${message}`)
|
|
183
|
+
const message = error instanceof Error ? error.message : "unknown network error"
|
|
184
|
+
throw new ScvdNetworkError(`Failed to fetch SCVD stats from ${url}: ${message}`)
|
|
189
185
|
}
|
|
190
186
|
|
|
191
187
|
if (!response.ok) {
|
|
192
188
|
throw new ScvdApiError(
|
|
193
189
|
response.status,
|
|
194
|
-
`Failed to fetch SCVD stats from ${url}: HTTP ${response.status}
|
|
195
|
-
)
|
|
190
|
+
`Failed to fetch SCVD stats from ${url}: HTTP ${response.status}`,
|
|
191
|
+
)
|
|
196
192
|
}
|
|
197
193
|
|
|
198
|
-
const body = (await response.json()) as unknown
|
|
199
|
-
return parseStats(body)
|
|
194
|
+
const body = (await response.json()) as unknown
|
|
195
|
+
return parseStats(body)
|
|
200
196
|
}
|
|
201
197
|
|
|
202
198
|
async fetchFindings(params: {
|
|
203
|
-
severity?: string
|
|
204
|
-
limit?: number
|
|
205
|
-
offset?: number
|
|
199
|
+
severity?: string
|
|
200
|
+
limit?: number
|
|
201
|
+
offset?: number
|
|
206
202
|
}): Promise<ScvdFinding[]> {
|
|
207
|
-
const searchParams = new URLSearchParams()
|
|
203
|
+
const searchParams = new URLSearchParams()
|
|
208
204
|
|
|
209
205
|
if (params.severity) {
|
|
210
|
-
searchParams.set("severity", params.severity)
|
|
206
|
+
searchParams.set("severity", params.severity)
|
|
211
207
|
}
|
|
212
208
|
if (typeof params.limit === "number") {
|
|
213
|
-
searchParams.set("limit", String(params.limit))
|
|
209
|
+
searchParams.set("limit", String(params.limit))
|
|
214
210
|
}
|
|
215
211
|
if (typeof params.offset === "number") {
|
|
216
|
-
searchParams.set("offset", String(params.offset))
|
|
212
|
+
searchParams.set("offset", String(params.offset))
|
|
217
213
|
}
|
|
218
214
|
|
|
219
|
-
const query = searchParams.toString()
|
|
220
|
-
const url = `${this.baseUrl}/findings${query.length > 0 ? `?${query}` : ""}
|
|
215
|
+
const query = searchParams.toString()
|
|
216
|
+
const url = `${this.baseUrl}/findings${query.length > 0 ? `?${query}` : ""}`
|
|
221
217
|
|
|
222
|
-
let response: Response
|
|
218
|
+
let response: Response
|
|
223
219
|
try {
|
|
224
|
-
response = await fetch(url, { signal: this.signal })
|
|
220
|
+
response = await fetch(url, { signal: this.signal })
|
|
225
221
|
} catch (error) {
|
|
226
|
-
const message = error instanceof Error ? error.message : "unknown network error"
|
|
227
|
-
throw new ScvdNetworkError(`Failed to fetch SCVD findings from ${url}: ${message}`)
|
|
222
|
+
const message = error instanceof Error ? error.message : "unknown network error"
|
|
223
|
+
throw new ScvdNetworkError(`Failed to fetch SCVD findings from ${url}: ${message}`)
|
|
228
224
|
}
|
|
229
225
|
|
|
230
226
|
if (!response.ok) {
|
|
231
|
-
throw new ScvdApiError(
|
|
232
|
-
response.status,
|
|
233
|
-
`SCVD API error: HTTP ${response.status} for ${url}`
|
|
234
|
-
);
|
|
227
|
+
throw new ScvdApiError(response.status, `SCVD API error: HTTP ${response.status} for ${url}`)
|
|
235
228
|
}
|
|
236
229
|
|
|
237
|
-
const body = (await response.json()) as unknown
|
|
238
|
-
return parseFindings(body)
|
|
230
|
+
const body = (await response.json()) as unknown
|
|
231
|
+
return parseFindings(body)
|
|
239
232
|
}
|
|
240
233
|
|
|
241
234
|
async fetchAllFindings(onProgress?: (count: number) => void): Promise<ScvdFinding[]> {
|
|
242
|
-
const results: ScvdFinding[] = []
|
|
243
|
-
let offset = 0
|
|
235
|
+
const results: ScvdFinding[] = []
|
|
236
|
+
let offset = 0
|
|
244
237
|
|
|
245
238
|
while (true) {
|
|
246
239
|
const page = await this.fetchFindings({
|
|
247
240
|
limit: DEFAULT_PAGE_SIZE,
|
|
248
241
|
offset,
|
|
249
|
-
})
|
|
242
|
+
})
|
|
250
243
|
|
|
251
244
|
if (page.length === 0) {
|
|
252
|
-
break
|
|
245
|
+
break
|
|
253
246
|
}
|
|
254
247
|
|
|
255
|
-
results.push(...page)
|
|
256
|
-
offset += page.length
|
|
248
|
+
results.push(...page)
|
|
249
|
+
offset += page.length
|
|
257
250
|
|
|
258
251
|
if (onProgress) {
|
|
259
|
-
onProgress(results.length)
|
|
252
|
+
onProgress(results.length)
|
|
260
253
|
}
|
|
261
254
|
|
|
262
255
|
if (page.length < DEFAULT_PAGE_SIZE) {
|
|
263
|
-
break
|
|
256
|
+
break
|
|
264
257
|
}
|
|
265
258
|
}
|
|
266
259
|
|
|
267
|
-
return results
|
|
260
|
+
return results
|
|
268
261
|
}
|
|
269
262
|
}
|
|
@@ -1,38 +1,38 @@
|
|
|
1
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
|
-
}
|
|
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
13
|
|
|
14
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
|
-
}
|
|
15
|
+
status: "success"
|
|
16
|
+
success: true
|
|
17
|
+
newFindings: number
|
|
18
|
+
totalIndexed: number
|
|
19
|
+
lastSync: string
|
|
20
|
+
error?: undefined
|
|
21
|
+
attempts?: number
|
|
22
|
+
}
|
|
23
23
|
|
|
24
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
|
-
}
|
|
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
34
|
|
|
35
|
-
export type SyncOutcome = SyncSuccess | SyncError | SyncStale
|
|
35
|
+
export type SyncOutcome = SyncSuccess | SyncError | SyncStale
|
|
36
36
|
|
|
37
37
|
export function createNetworkError(message: string): SyncError {
|
|
38
38
|
return {
|
|
@@ -44,7 +44,7 @@ export function createNetworkError(message: string): SyncError {
|
|
|
44
44
|
newFindings: 0,
|
|
45
45
|
totalIndexed: 0,
|
|
46
46
|
lastSync: new Date().toISOString(),
|
|
47
|
-
}
|
|
47
|
+
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export function createApiError(httpStatus: number, message: string): SyncError {
|
|
@@ -58,7 +58,7 @@ export function createApiError(httpStatus: number, message: string): SyncError {
|
|
|
58
58
|
newFindings: 0,
|
|
59
59
|
totalIndexed: 0,
|
|
60
60
|
lastSync: new Date().toISOString(),
|
|
61
|
-
}
|
|
61
|
+
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function createParseError(message: string): SyncError {
|
|
@@ -71,19 +71,19 @@ export function createParseError(message: string): SyncError {
|
|
|
71
71
|
newFindings: 0,
|
|
72
72
|
totalIndexed: 0,
|
|
73
73
|
lastSync: new Date().toISOString(),
|
|
74
|
-
}
|
|
74
|
+
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export function createSyncSuccess(
|
|
78
|
-
data: Omit<SyncSuccess, "status" | "success" | "error"> & { attempts?: number }
|
|
78
|
+
data: Omit<SyncSuccess, "status" | "success" | "error"> & { attempts?: number },
|
|
79
79
|
): SyncSuccess {
|
|
80
80
|
return {
|
|
81
81
|
status: "success",
|
|
82
82
|
success: true,
|
|
83
83
|
...data,
|
|
84
|
-
}
|
|
84
|
+
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function isRetryableError(outcome: SyncOutcome): boolean {
|
|
88
|
-
return outcome.status === "error" && outcome.reason === "network"
|
|
88
|
+
return outcome.status === "error" && outcome.reason === "network"
|
|
89
89
|
}
|