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
|
@@ -1,50 +1,50 @@
|
|
|
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
11
|
}
|
|
12
12
|
|
|
13
13
|
export interface ScvdIndexMetadata {
|
|
14
|
-
lastSuccess: string | null
|
|
15
|
-
lastAttempt: string | null
|
|
16
|
-
errorCount: number
|
|
17
|
-
lastError: string | null
|
|
18
|
-
lastErrorReason: string | null
|
|
14
|
+
lastSuccess: string | null
|
|
15
|
+
lastAttempt: string | null
|
|
16
|
+
errorCount: number
|
|
17
|
+
lastError: string | null
|
|
18
|
+
lastErrorReason: string | null
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface ScvdIndex {
|
|
22
|
-
version: number
|
|
23
|
-
lastSync: string
|
|
24
|
-
totalFindings: number
|
|
25
|
-
entries: ScvdIndexEntry[]
|
|
26
|
-
metadata?: ScvdIndexMetadata
|
|
22
|
+
version: number
|
|
23
|
+
lastSync: string
|
|
24
|
+
totalFindings: number
|
|
25
|
+
entries: ScvdIndexEntry[]
|
|
26
|
+
metadata?: ScvdIndexMetadata
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const INDEX_VERSION = 1
|
|
30
|
-
const DEFAULT_LIMIT = 10
|
|
31
|
-
let syncInProgress = false
|
|
29
|
+
const INDEX_VERSION = 1
|
|
30
|
+
const DEFAULT_LIMIT = 10
|
|
31
|
+
let syncInProgress = false
|
|
32
32
|
|
|
33
33
|
export function acquireSyncLock(): boolean {
|
|
34
34
|
if (syncInProgress) {
|
|
35
|
-
return false
|
|
35
|
+
return false
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
syncInProgress = true
|
|
39
|
-
return true
|
|
38
|
+
syncInProgress = true
|
|
39
|
+
return true
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function releaseSyncLock(): void {
|
|
43
|
-
syncInProgress = false
|
|
43
|
+
syncInProgress = false
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
export function isSyncLocked(): boolean {
|
|
47
|
-
return syncInProgress
|
|
47
|
+
return syncInProgress
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function normalizeKeywordInput(value: string): string[] {
|
|
@@ -52,15 +52,15 @@ function normalizeKeywordInput(value: string): string[] {
|
|
|
52
52
|
.toLowerCase()
|
|
53
53
|
.split(/[^a-z0-9]+/g)
|
|
54
54
|
.map((word) => word.trim())
|
|
55
|
-
.filter((word) => word.length > 1)
|
|
55
|
+
.filter((word) => word.length > 1)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
function uniqueWords(words: string[]): string[] {
|
|
59
|
-
return Array.from(new Set(words))
|
|
59
|
+
return Array.from(new Set(words))
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function findingToEntry(finding: ScvdFinding): ScvdIndexEntry {
|
|
63
|
-
const keywordSource = `${finding.title} ${finding.description_md}
|
|
63
|
+
const keywordSource = `${finding.title} ${finding.description_md}`
|
|
64
64
|
|
|
65
65
|
return {
|
|
66
66
|
id: finding.scvd_id,
|
|
@@ -70,86 +70,84 @@ function findingToEntry(finding: ScvdFinding): ScvdIndexEntry {
|
|
|
70
70
|
cwe: finding.taxonomy.cwe,
|
|
71
71
|
keywords: uniqueWords(normalizeKeywordInput(keywordSource)),
|
|
72
72
|
repoUrl: finding.repo.url,
|
|
73
|
-
}
|
|
73
|
+
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export function buildIndex(findings: ScvdFinding[]): ScvdIndex {
|
|
77
|
-
const now = new Date().toISOString()
|
|
78
|
-
const entries = findings.map(findingToEntry)
|
|
77
|
+
const now = new Date().toISOString()
|
|
78
|
+
const entries = findings.map(findingToEntry)
|
|
79
79
|
|
|
80
80
|
return {
|
|
81
81
|
version: INDEX_VERSION,
|
|
82
82
|
lastSync: now,
|
|
83
83
|
totalFindings: entries.length,
|
|
84
84
|
entries,
|
|
85
|
-
}
|
|
85
|
+
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export function searchIndex(
|
|
89
89
|
index: ScvdIndex,
|
|
90
90
|
query: {
|
|
91
|
-
swc?: string
|
|
92
|
-
severity?: string
|
|
93
|
-
keyword?: string
|
|
94
|
-
limit?: number
|
|
95
|
-
}
|
|
91
|
+
swc?: string
|
|
92
|
+
severity?: string
|
|
93
|
+
keyword?: string
|
|
94
|
+
limit?: number
|
|
95
|
+
},
|
|
96
96
|
): ScvdIndexEntry[] {
|
|
97
|
-
const normalizedKeyword = query.keyword?.toLowerCase().trim()
|
|
98
|
-
const limit = query.limit ?? DEFAULT_LIMIT
|
|
97
|
+
const normalizedKeyword = query.keyword?.toLowerCase().trim()
|
|
98
|
+
const limit = query.limit ?? DEFAULT_LIMIT
|
|
99
99
|
|
|
100
100
|
const filtered = index.entries.filter((entry) => {
|
|
101
101
|
if (query.swc && !entry.swc.includes(query.swc)) {
|
|
102
|
-
return false
|
|
102
|
+
return false
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
if (query.severity && entry.severity !== query.severity) {
|
|
106
|
-
return false
|
|
106
|
+
return false
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
if (normalizedKeyword && normalizedKeyword.length > 0) {
|
|
110
|
-
const matchesKeyword = entry.keywords.some((keyword) =>
|
|
111
|
-
keyword.includes(normalizedKeyword)
|
|
112
|
-
);
|
|
110
|
+
const matchesKeyword = entry.keywords.some((keyword) => keyword.includes(normalizedKeyword))
|
|
113
111
|
|
|
114
112
|
if (!matchesKeyword) {
|
|
115
|
-
return false
|
|
113
|
+
return false
|
|
116
114
|
}
|
|
117
115
|
}
|
|
118
116
|
|
|
119
|
-
return true
|
|
120
|
-
})
|
|
117
|
+
return true
|
|
118
|
+
})
|
|
121
119
|
|
|
122
|
-
return filtered.slice(0, limit)
|
|
120
|
+
return filtered.slice(0, limit)
|
|
123
121
|
}
|
|
124
122
|
|
|
125
123
|
export async function saveIndex(index: ScvdIndex, filePath: string): Promise<void> {
|
|
126
|
-
const tmpPath = `${filePath}.tmp.${Date.now()}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
renameSync(tmpPath, filePath)
|
|
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)
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
133
|
-
return typeof value === "object" && value !== null
|
|
131
|
+
return typeof value === "object" && value !== null
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
function parseStringArray(value: unknown): string[] {
|
|
137
135
|
if (!Array.isArray(value)) {
|
|
138
|
-
return []
|
|
136
|
+
return []
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
return value.filter((item): item is string => typeof item === "string")
|
|
139
|
+
return value.filter((item): item is string => typeof item === "string")
|
|
142
140
|
}
|
|
143
141
|
|
|
144
142
|
function parseEntry(value: unknown): ScvdIndexEntry | null {
|
|
145
143
|
if (!isRecord(value)) {
|
|
146
|
-
return null
|
|
144
|
+
return null
|
|
147
145
|
}
|
|
148
146
|
|
|
149
|
-
const id = value.id
|
|
150
|
-
const title = value.title
|
|
151
|
-
const severity = value.severity
|
|
152
|
-
const repoUrl = value.repoUrl
|
|
147
|
+
const id = value.id
|
|
148
|
+
const title = value.title
|
|
149
|
+
const severity = value.severity
|
|
150
|
+
const repoUrl = value.repoUrl
|
|
153
151
|
|
|
154
152
|
if (
|
|
155
153
|
typeof id !== "string" ||
|
|
@@ -157,7 +155,7 @@ function parseEntry(value: unknown): ScvdIndexEntry | null {
|
|
|
157
155
|
typeof severity !== "string" ||
|
|
158
156
|
typeof repoUrl !== "string"
|
|
159
157
|
) {
|
|
160
|
-
return null
|
|
158
|
+
return null
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
return {
|
|
@@ -168,11 +166,11 @@ function parseEntry(value: unknown): ScvdIndexEntry | null {
|
|
|
168
166
|
cwe: parseStringArray(value.cwe),
|
|
169
167
|
keywords: parseStringArray(value.keywords),
|
|
170
168
|
repoUrl,
|
|
171
|
-
}
|
|
169
|
+
}
|
|
172
170
|
}
|
|
173
171
|
|
|
174
172
|
function parseNullableString(value: unknown): string | null {
|
|
175
|
-
return typeof value === "string" ? value : null
|
|
173
|
+
return typeof value === "string" ? value : null
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
function parseMetadata(raw: Record<string, unknown>): ScvdIndexMetadata {
|
|
@@ -182,27 +180,27 @@ function parseMetadata(raw: Record<string, unknown>): ScvdIndexMetadata {
|
|
|
182
180
|
errorCount: typeof raw.errorCount === "number" ? raw.errorCount : 0,
|
|
183
181
|
lastError: parseNullableString(raw.lastError),
|
|
184
182
|
lastErrorReason: parseNullableString(raw.lastErrorReason),
|
|
185
|
-
}
|
|
183
|
+
}
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
export async function loadIndex(filePath: string): Promise<ScvdIndex | null> {
|
|
189
|
-
const file = Bun.file(filePath)
|
|
190
|
-
const exists = await file.exists()
|
|
187
|
+
const file = Bun.file(filePath)
|
|
188
|
+
const exists = await file.exists()
|
|
191
189
|
|
|
192
190
|
if (!exists) {
|
|
193
|
-
return null
|
|
191
|
+
return null
|
|
194
192
|
}
|
|
195
193
|
|
|
196
|
-
const raw = (await file.json()) as unknown
|
|
194
|
+
const raw = (await file.json()) as unknown
|
|
197
195
|
|
|
198
196
|
if (!isRecord(raw)) {
|
|
199
|
-
return null
|
|
197
|
+
return null
|
|
200
198
|
}
|
|
201
199
|
|
|
202
|
-
const version = raw.version
|
|
203
|
-
const lastSync = raw.lastSync
|
|
204
|
-
const totalFindings = raw.totalFindings
|
|
205
|
-
const rawEntries = raw.entries
|
|
200
|
+
const version = raw.version
|
|
201
|
+
const lastSync = raw.lastSync
|
|
202
|
+
const totalFindings = raw.totalFindings
|
|
203
|
+
const rawEntries = raw.entries
|
|
206
204
|
|
|
207
205
|
if (
|
|
208
206
|
typeof version !== "number" ||
|
|
@@ -210,24 +208,24 @@ export async function loadIndex(filePath: string): Promise<ScvdIndex | null> {
|
|
|
210
208
|
typeof totalFindings !== "number" ||
|
|
211
209
|
!Array.isArray(rawEntries)
|
|
212
210
|
) {
|
|
213
|
-
return null
|
|
211
|
+
return null
|
|
214
212
|
}
|
|
215
213
|
|
|
216
214
|
const entries = rawEntries
|
|
217
215
|
.map(parseEntry)
|
|
218
|
-
.filter((entry): entry is ScvdIndexEntry => entry !== null)
|
|
216
|
+
.filter((entry): entry is ScvdIndexEntry => entry !== null)
|
|
219
217
|
|
|
220
218
|
const index: ScvdIndex = {
|
|
221
219
|
version,
|
|
222
220
|
lastSync,
|
|
223
221
|
totalFindings,
|
|
224
222
|
entries,
|
|
225
|
-
}
|
|
223
|
+
}
|
|
226
224
|
|
|
227
|
-
const rawMetadata = raw.metadata
|
|
225
|
+
const rawMetadata = raw.metadata
|
|
228
226
|
if (isRecord(rawMetadata)) {
|
|
229
|
-
index.metadata = parseMetadata(rawMetadata)
|
|
227
|
+
index.metadata = parseMetadata(rawMetadata)
|
|
230
228
|
}
|
|
231
229
|
|
|
232
|
-
return index
|
|
230
|
+
return index
|
|
233
231
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createLogger } from "../shared/logger"
|
|
2
|
+
import { withRetry } from "./retry"
|
|
3
|
+
import type { ScvdClient } from "./scvd-client"
|
|
4
|
+
import { ScvdApiError, ScvdNetworkError } from "./scvd-client"
|
|
4
5
|
import {
|
|
5
6
|
createApiError,
|
|
6
7
|
createNetworkError,
|
|
@@ -9,64 +10,60 @@ import {
|
|
|
9
10
|
isRetryableError,
|
|
10
11
|
type SyncError,
|
|
11
12
|
type SyncOutcome,
|
|
12
|
-
} from "./scvd-errors"
|
|
13
|
+
} from "./scvd-errors"
|
|
13
14
|
import {
|
|
14
15
|
acquireSyncLock,
|
|
15
16
|
buildIndex,
|
|
16
17
|
loadIndex,
|
|
17
18
|
releaseSyncLock,
|
|
18
|
-
saveIndex,
|
|
19
19
|
type ScvdIndex,
|
|
20
20
|
type ScvdIndexMetadata,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
saveIndex,
|
|
22
|
+
} from "./scvd-index"
|
|
23
23
|
|
|
24
|
-
export type SyncResult = SyncOutcome
|
|
24
|
+
export type SyncResult = SyncOutcome
|
|
25
25
|
|
|
26
|
-
const RETRY_MAX_ATTEMPTS = 3
|
|
27
|
-
const RETRY_BASE_DELAY_MS = 1000
|
|
26
|
+
const RETRY_MAX_ATTEMPTS = 3
|
|
27
|
+
const RETRY_BASE_DELAY_MS = 1000
|
|
28
28
|
|
|
29
29
|
function buildErrorResult(error: unknown): SyncError {
|
|
30
|
-
const message = error instanceof Error ? error.message : "Unknown sync error"
|
|
30
|
+
const message = error instanceof Error ? error.message : "Unknown sync error"
|
|
31
31
|
|
|
32
32
|
if (error instanceof ScvdNetworkError) {
|
|
33
|
-
return createNetworkError(message)
|
|
33
|
+
return createNetworkError(message)
|
|
34
34
|
}
|
|
35
35
|
if (error instanceof ScvdApiError) {
|
|
36
|
-
return createApiError(error.httpStatus, message)
|
|
36
|
+
return createApiError(error.httpStatus, message)
|
|
37
37
|
}
|
|
38
|
-
return createParseError(message)
|
|
38
|
+
return createParseError(message)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function shouldRetrySyncError(error: unknown): boolean {
|
|
42
42
|
if (!(error instanceof ScvdNetworkError)) {
|
|
43
|
-
return false
|
|
43
|
+
return false
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
return isRetryableError(buildErrorResult(error))
|
|
46
|
+
return isRetryableError(buildErrorResult(error))
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function errorReasonFromResult(result: SyncError): string {
|
|
50
|
-
return result.reason
|
|
50
|
+
return result.reason
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async function persistErrorMetadata(
|
|
54
|
-
indexPath
|
|
55
|
-
|
|
56
|
-
): Promise<void> {
|
|
57
|
-
const existing = await loadIndex(indexPath);
|
|
58
|
-
if (!existing) return;
|
|
53
|
+
async function persistErrorMetadata(indexPath: string, errorResult: SyncError): Promise<void> {
|
|
54
|
+
const existing = await loadIndex(indexPath)
|
|
55
|
+
if (!existing) return
|
|
59
56
|
|
|
60
|
-
const now = new Date().toISOString()
|
|
61
|
-
const prevMetadata = existing.metadata
|
|
57
|
+
const now = new Date().toISOString()
|
|
58
|
+
const prevMetadata = existing.metadata
|
|
62
59
|
existing.metadata = {
|
|
63
60
|
lastSuccess: prevMetadata?.lastSuccess ?? null,
|
|
64
61
|
lastAttempt: now,
|
|
65
62
|
errorCount: (prevMetadata?.errorCount ?? 0) + 1,
|
|
66
63
|
lastError: errorResult.message,
|
|
67
64
|
lastErrorReason: errorReasonFromResult(errorResult),
|
|
68
|
-
}
|
|
69
|
-
await saveIndex(existing, indexPath)
|
|
65
|
+
}
|
|
66
|
+
await saveIndex(existing, indexPath)
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
async function syncAllUnlocked(client: ScvdClient, indexPath: string): Promise<SyncResult> {
|
|
@@ -74,81 +71,83 @@ async function syncAllUnlocked(client: ScvdClient, indexPath: string): Promise<S
|
|
|
74
71
|
maxAttempts: RETRY_MAX_ATTEMPTS,
|
|
75
72
|
baseDelayMs: RETRY_BASE_DELAY_MS,
|
|
76
73
|
shouldRetry: shouldRetrySyncError,
|
|
77
|
-
})
|
|
74
|
+
})
|
|
78
75
|
|
|
79
76
|
if (!fetchResult.success) {
|
|
80
|
-
const errorResult = buildErrorResult(fetchResult.error)
|
|
81
|
-
errorResult.attempts = fetchResult.attempts
|
|
82
|
-
await persistErrorMetadata(indexPath, errorResult)
|
|
83
|
-
return errorResult
|
|
77
|
+
const errorResult = buildErrorResult(fetchResult.error)
|
|
78
|
+
errorResult.attempts = fetchResult.attempts
|
|
79
|
+
await persistErrorMetadata(indexPath, errorResult)
|
|
80
|
+
return errorResult
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
if (fetchResult.value === undefined) {
|
|
87
|
-
const errorResult = createParseError("SCVD sync returned no findings payload")
|
|
88
|
-
errorResult.attempts = fetchResult.attempts
|
|
89
|
-
await persistErrorMetadata(indexPath, errorResult)
|
|
90
|
-
return errorResult
|
|
84
|
+
const errorResult = createParseError("SCVD sync returned no findings payload")
|
|
85
|
+
errorResult.attempts = fetchResult.attempts
|
|
86
|
+
await persistErrorMetadata(indexPath, errorResult)
|
|
87
|
+
return errorResult
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
const findings = fetchResult.value
|
|
94
|
-
const index = buildIndex(findings)
|
|
95
|
-
const now = new Date().toISOString()
|
|
90
|
+
const findings = fetchResult.value
|
|
91
|
+
const index = buildIndex(findings)
|
|
92
|
+
const now = new Date().toISOString()
|
|
96
93
|
index.metadata = {
|
|
97
94
|
lastSuccess: now,
|
|
98
95
|
lastAttempt: now,
|
|
99
96
|
errorCount: 0,
|
|
100
97
|
lastError: null,
|
|
101
98
|
lastErrorReason: null,
|
|
102
|
-
}
|
|
103
|
-
await saveIndex(index, indexPath)
|
|
99
|
+
}
|
|
100
|
+
await saveIndex(index, indexPath)
|
|
104
101
|
|
|
105
102
|
return createSyncSuccess({
|
|
106
103
|
newFindings: findings.length,
|
|
107
104
|
totalIndexed: index.totalFindings,
|
|
108
105
|
lastSync: index.lastSync,
|
|
109
106
|
attempts: fetchResult.attempts,
|
|
110
|
-
})
|
|
107
|
+
})
|
|
111
108
|
}
|
|
112
109
|
|
|
113
110
|
export async function syncAll(client: ScvdClient, indexPath: string): Promise<SyncResult> {
|
|
114
|
-
const logger = createLogger()
|
|
111
|
+
const logger = createLogger()
|
|
115
112
|
|
|
116
113
|
if (!acquireSyncLock()) {
|
|
117
|
-
return createParseError("Sync already in progress")
|
|
114
|
+
return createParseError("Sync already in progress")
|
|
118
115
|
}
|
|
119
116
|
|
|
120
|
-
logger.debug("[sync] starting", "source=scvd mode=full")
|
|
117
|
+
logger.debug("[sync] starting", "source=scvd mode=full")
|
|
121
118
|
|
|
122
119
|
try {
|
|
123
|
-
const result = await syncAllUnlocked(client, indexPath)
|
|
120
|
+
const result = await syncAllUnlocked(client, indexPath)
|
|
124
121
|
if (result.success) {
|
|
125
|
-
logger.debug(
|
|
122
|
+
logger.debug(
|
|
123
|
+
"[sync] complete",
|
|
124
|
+
`source=scvd newFindings=${result.newFindings} totalIndexed=${result.totalIndexed}`,
|
|
125
|
+
)
|
|
126
126
|
} else {
|
|
127
|
-
const reason = result.status === "error" ? result.reason : result.status
|
|
128
|
-
logger.debug("[sync] failed", `source=scvd reason=${reason}`)
|
|
127
|
+
const reason = result.status === "error" ? result.reason : result.status
|
|
128
|
+
logger.debug("[sync] failed", `source=scvd reason=${reason}`)
|
|
129
129
|
}
|
|
130
|
-
return result
|
|
130
|
+
return result
|
|
131
131
|
} catch (error) {
|
|
132
|
-
const errorResult = buildErrorResult(error)
|
|
133
|
-
logger.debug("[sync] failed", `source=scvd reason=${errorResult.reason}`)
|
|
134
|
-
await persistErrorMetadata(indexPath, errorResult).catch(() => {
|
|
135
|
-
|
|
132
|
+
const errorResult = buildErrorResult(error)
|
|
133
|
+
logger.debug("[sync] failed", `source=scvd reason=${errorResult.reason}`)
|
|
134
|
+
await persistErrorMetadata(indexPath, errorResult).catch(() => {
|
|
135
|
+
logger.debug("Failed to persist sync error metadata")
|
|
136
|
+
})
|
|
137
|
+
return errorResult
|
|
136
138
|
} finally {
|
|
137
|
-
releaseSyncLock()
|
|
139
|
+
releaseSyncLock()
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
export async function syncIncremental(
|
|
142
|
-
|
|
143
|
-
indexPath: string
|
|
144
|
-
): Promise<SyncResult> {
|
|
145
|
-
const logger = createLogger();
|
|
143
|
+
export async function syncIncremental(client: ScvdClient, indexPath: string): Promise<SyncResult> {
|
|
144
|
+
const logger = createLogger()
|
|
146
145
|
|
|
147
146
|
if (!acquireSyncLock()) {
|
|
148
|
-
return createParseError("Sync already in progress")
|
|
147
|
+
return createParseError("Sync already in progress")
|
|
149
148
|
}
|
|
150
149
|
|
|
151
|
-
logger.debug("[sync] starting", "source=scvd mode=incremental")
|
|
150
|
+
logger.debug("[sync] starting", "source=scvd mode=incremental")
|
|
152
151
|
|
|
153
152
|
try {
|
|
154
153
|
const [statsResult, existingIndex] = await Promise.all([
|
|
@@ -158,65 +157,71 @@ export async function syncIncremental(
|
|
|
158
157
|
shouldRetry: shouldRetrySyncError,
|
|
159
158
|
}),
|
|
160
159
|
loadIndex(indexPath),
|
|
161
|
-
])
|
|
160
|
+
])
|
|
162
161
|
|
|
163
162
|
if (!statsResult.success) {
|
|
164
|
-
const errorResult = buildErrorResult(statsResult.error)
|
|
165
|
-
errorResult.attempts = statsResult.attempts
|
|
166
|
-
await persistErrorMetadata(indexPath, errorResult).catch(() => {
|
|
167
|
-
|
|
163
|
+
const errorResult = buildErrorResult(statsResult.error)
|
|
164
|
+
errorResult.attempts = statsResult.attempts
|
|
165
|
+
await persistErrorMetadata(indexPath, errorResult).catch(() => {
|
|
166
|
+
logger.debug("Failed to persist sync error metadata")
|
|
167
|
+
})
|
|
168
|
+
return errorResult
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
if (statsResult.value === undefined) {
|
|
171
|
-
const errorResult = createParseError("SCVD sync returned no stats payload")
|
|
172
|
-
errorResult.attempts = statsResult.attempts
|
|
173
|
-
await persistErrorMetadata(indexPath, errorResult).catch(() => {
|
|
174
|
-
|
|
172
|
+
const errorResult = createParseError("SCVD sync returned no stats payload")
|
|
173
|
+
errorResult.attempts = statsResult.attempts
|
|
174
|
+
await persistErrorMetadata(indexPath, errorResult).catch(() => {
|
|
175
|
+
logger.debug("Failed to persist sync error metadata")
|
|
176
|
+
})
|
|
177
|
+
return errorResult
|
|
175
178
|
}
|
|
176
179
|
|
|
177
|
-
const stats = statsResult.value
|
|
180
|
+
const stats = statsResult.value
|
|
178
181
|
|
|
179
182
|
if (existingIndex && existingIndex.totalFindings === stats.total) {
|
|
180
183
|
return createSyncSuccess({
|
|
181
184
|
newFindings: 0,
|
|
182
185
|
totalIndexed: existingIndex.totalFindings,
|
|
183
186
|
lastSync: existingIndex.lastSync,
|
|
184
|
-
})
|
|
187
|
+
})
|
|
185
188
|
}
|
|
186
189
|
|
|
187
|
-
return await syncAllUnlocked(client, indexPath)
|
|
190
|
+
return await syncAllUnlocked(client, indexPath)
|
|
188
191
|
} catch (error) {
|
|
189
|
-
const errorResult = buildErrorResult(error)
|
|
190
|
-
await persistErrorMetadata(indexPath, errorResult).catch(() => {
|
|
191
|
-
|
|
192
|
+
const errorResult = buildErrorResult(error)
|
|
193
|
+
await persistErrorMetadata(indexPath, errorResult).catch(() => {
|
|
194
|
+
logger.debug("Failed to persist sync error metadata")
|
|
195
|
+
})
|
|
196
|
+
return errorResult
|
|
192
197
|
} finally {
|
|
193
|
-
releaseSyncLock()
|
|
198
|
+
releaseSyncLock()
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
|
-
const STALE_THRESHOLD_DAYS = 7
|
|
201
|
+
const STALE_THRESHOLD_DAYS = 7
|
|
197
202
|
|
|
198
203
|
export function isSyncStale(
|
|
199
204
|
index: ScvdIndex | null,
|
|
200
|
-
thresholdDays: number = STALE_THRESHOLD_DAYS
|
|
205
|
+
thresholdDays: number = STALE_THRESHOLD_DAYS,
|
|
201
206
|
): boolean {
|
|
202
|
-
if (!index || !index.lastSync) return true
|
|
203
|
-
const lastSyncDate = new Date(index.lastSync)
|
|
204
|
-
const now = new Date()
|
|
205
|
-
const diffMs = now.getTime() - lastSyncDate.getTime()
|
|
206
|
-
const diffDays = diffMs / (1000 * 60 * 60 * 24)
|
|
207
|
-
return diffDays > thresholdDays
|
|
207
|
+
if (!index || !index.lastSync) return true
|
|
208
|
+
const lastSyncDate = new Date(index.lastSync)
|
|
209
|
+
const now = new Date()
|
|
210
|
+
const diffMs = now.getTime() - lastSyncDate.getTime()
|
|
211
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24)
|
|
212
|
+
return diffDays > thresholdDays
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
export async function getSyncStatus(indexPath: string): Promise<{
|
|
211
|
-
lastSync: string | null
|
|
212
|
-
totalFindings: number
|
|
213
|
-
healthy: boolean
|
|
214
|
-
stale: boolean
|
|
215
|
-
metadata: ScvdIndexMetadata | null
|
|
216
|
-
hint?: string
|
|
216
|
+
lastSync: string | null
|
|
217
|
+
totalFindings: number
|
|
218
|
+
healthy: boolean
|
|
219
|
+
stale: boolean
|
|
220
|
+
metadata: ScvdIndexMetadata | null
|
|
221
|
+
hint?: string
|
|
217
222
|
}> {
|
|
218
|
-
const logger = createLogger()
|
|
219
|
-
const index = await loadIndex(indexPath)
|
|
223
|
+
const logger = createLogger()
|
|
224
|
+
const index = await loadIndex(indexPath)
|
|
220
225
|
|
|
221
226
|
if (!index) {
|
|
222
227
|
return {
|
|
@@ -226,15 +231,15 @@ export async function getSyncStatus(indexPath: string): Promise<{
|
|
|
226
231
|
stale: true,
|
|
227
232
|
metadata: null,
|
|
228
233
|
hint: "SCVD data is missing. Run argus_sync_knowledge to populate.",
|
|
229
|
-
}
|
|
234
|
+
}
|
|
230
235
|
}
|
|
231
236
|
|
|
232
|
-
const stale = isSyncStale(index)
|
|
237
|
+
const stale = isSyncStale(index)
|
|
233
238
|
|
|
234
239
|
if (stale) {
|
|
235
|
-
const lastSyncDate = new Date(index.lastSync)
|
|
236
|
-
const daysSince = Math.floor((Date.now() - lastSyncDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
237
|
-
logger.debug("[sync] stale", `source=scvd daysSince=${daysSince}`)
|
|
240
|
+
const lastSyncDate = new Date(index.lastSync)
|
|
241
|
+
const daysSince = Math.floor((Date.now() - lastSyncDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
242
|
+
logger.debug("[sync] stale", `source=scvd daysSince=${daysSince}`)
|
|
238
243
|
|
|
239
244
|
return {
|
|
240
245
|
lastSync: index.lastSync,
|
|
@@ -243,7 +248,7 @@ export async function getSyncStatus(indexPath: string): Promise<{
|
|
|
243
248
|
stale: true,
|
|
244
249
|
metadata: index.metadata ?? null,
|
|
245
250
|
hint: "SCVD data is stale. Run argus_sync_knowledge to update.",
|
|
246
|
-
}
|
|
251
|
+
}
|
|
247
252
|
}
|
|
248
253
|
|
|
249
254
|
return {
|
|
@@ -252,5 +257,5 @@ export async function getSyncStatus(indexPath: string): Promise<{
|
|
|
252
257
|
healthy: true,
|
|
253
258
|
stale: false,
|
|
254
259
|
metadata: index.metadata ?? null,
|
|
255
|
-
}
|
|
260
|
+
}
|
|
256
261
|
}
|
package/src/managers/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { AuditStateManager, BackgroundManager, Managers } from "./types"
|