solidity-argus 0.2.0 → 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 +93 -37
- package/package.json +33 -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 +24 -7
- package/src/agents/pythia-prompt.ts +3 -4
- package/src/agents/scribe-prompt.ts +7 -2
- package/src/agents/sentinel-prompt.ts +32 -3
- 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 +4 -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/tool-tracking-hook.ts +104 -50
- package/src/hooks/types.ts +2 -1
- package/src/index.ts +23 -36
- 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 +202 -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 +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 +185 -190
- package/src/tools/pattern-loader.ts +5 -111
- package/src/tools/proxy-detection-tool.ts +224 -0
- package/src/tools/report-generator-tool.ts +268 -200
- package/src/tools/slither-tool.ts +266 -218
- package/src/tools/solodit-search-tool.ts +216 -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 +103 -74
- 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,37 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { AuditState, Finding, FindingSeverity
|
|
1
|
+
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
2
|
+
import type { AuditState, Finding, FindingSeverity } from "../state/types"
|
|
3
3
|
|
|
4
|
-
type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational"
|
|
4
|
+
type SeverityThreshold = "critical" | "high" | "medium" | "low" | "informational"
|
|
5
5
|
|
|
6
6
|
type ReportGeneratorArgs = {
|
|
7
|
-
project_name: string
|
|
8
|
-
scope: string[]
|
|
9
|
-
include_executive_summary?: boolean
|
|
10
|
-
severity_threshold?: SeverityThreshold
|
|
11
|
-
audit_state: string
|
|
12
|
-
}
|
|
7
|
+
project_name: string
|
|
8
|
+
scope: string[]
|
|
9
|
+
include_executive_summary?: boolean
|
|
10
|
+
severity_threshold?: SeverityThreshold
|
|
11
|
+
audit_state: string
|
|
12
|
+
}
|
|
13
13
|
|
|
14
14
|
type FindingsCount = {
|
|
15
|
-
critical: number
|
|
16
|
-
high: number
|
|
17
|
-
medium: number
|
|
18
|
-
low: number
|
|
19
|
-
informational: number
|
|
20
|
-
}
|
|
15
|
+
critical: number
|
|
16
|
+
high: number
|
|
17
|
+
medium: number
|
|
18
|
+
low: number
|
|
19
|
+
informational: number
|
|
20
|
+
}
|
|
21
21
|
|
|
22
22
|
export type ReportGenerationResult = {
|
|
23
|
-
report: string
|
|
24
|
-
findingsCount: FindingsCount
|
|
25
|
-
filename: string
|
|
26
|
-
}
|
|
23
|
+
report: string
|
|
24
|
+
findingsCount: FindingsCount
|
|
25
|
+
filename: string
|
|
26
|
+
}
|
|
27
27
|
|
|
28
|
-
const SEVERITY_ORDER: FindingSeverity[] = [
|
|
29
|
-
"Critical",
|
|
30
|
-
"High",
|
|
31
|
-
"Medium",
|
|
32
|
-
"Low",
|
|
33
|
-
"Informational",
|
|
34
|
-
];
|
|
28
|
+
const SEVERITY_ORDER: FindingSeverity[] = ["Critical", "High", "Medium", "Low", "Informational"]
|
|
35
29
|
|
|
36
30
|
const SEVERITY_PREFIX: Record<FindingSeverity, string> = {
|
|
37
31
|
Critical: "CRIT",
|
|
@@ -39,7 +33,7 @@ const SEVERITY_PREFIX: Record<FindingSeverity, string> = {
|
|
|
39
33
|
Medium: "MED",
|
|
40
34
|
Low: "LOW",
|
|
41
35
|
Informational: "INFO",
|
|
42
|
-
}
|
|
36
|
+
}
|
|
43
37
|
|
|
44
38
|
const THRESHOLD_WEIGHT: Record<SeverityThreshold, number> = {
|
|
45
39
|
critical: 5,
|
|
@@ -47,7 +41,7 @@ const THRESHOLD_WEIGHT: Record<SeverityThreshold, number> = {
|
|
|
47
41
|
medium: 3,
|
|
48
42
|
low: 2,
|
|
49
43
|
informational: 1,
|
|
50
|
-
}
|
|
44
|
+
}
|
|
51
45
|
|
|
52
46
|
const FINDING_WEIGHT: Record<FindingSeverity, number> = {
|
|
53
47
|
Critical: 5,
|
|
@@ -55,7 +49,7 @@ const FINDING_WEIGHT: Record<FindingSeverity, number> = {
|
|
|
55
49
|
Medium: 3,
|
|
56
50
|
Low: 2,
|
|
57
51
|
Informational: 1,
|
|
58
|
-
}
|
|
52
|
+
}
|
|
59
53
|
|
|
60
54
|
function emptyCounts(): FindingsCount {
|
|
61
55
|
return {
|
|
@@ -64,7 +58,7 @@ function emptyCounts(): FindingsCount {
|
|
|
64
58
|
medium: 0,
|
|
65
59
|
low: 0,
|
|
66
60
|
informational: 0,
|
|
67
|
-
}
|
|
61
|
+
}
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
function emptyAuditState(findings: Finding[] = []): AuditState {
|
|
@@ -77,164 +71,247 @@ function emptyAuditState(findings: Finding[] = []): AuditState {
|
|
|
77
71
|
currentPhase: "complete",
|
|
78
72
|
scope: [],
|
|
79
73
|
startTime: 0,
|
|
80
|
-
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hasMinimumFindingFields(
|
|
78
|
+
f: unknown,
|
|
79
|
+
): f is { check: string; file: string; lines: [number, number] } {
|
|
80
|
+
if (typeof f !== "object" || f === null) return false
|
|
81
|
+
const obj = f as Record<string, unknown>
|
|
82
|
+
return (
|
|
83
|
+
typeof obj.check === "string" &&
|
|
84
|
+
obj.check.length > 0 &&
|
|
85
|
+
typeof obj.file === "string" &&
|
|
86
|
+
Array.isArray(obj.lines) &&
|
|
87
|
+
obj.lines.length === 2
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const VALID_SEVERITIES: ReadonlySet<string> = new Set([
|
|
92
|
+
"Critical",
|
|
93
|
+
"High",
|
|
94
|
+
"Medium",
|
|
95
|
+
"Low",
|
|
96
|
+
"Informational",
|
|
97
|
+
])
|
|
98
|
+
const VALID_SOURCES: ReadonlySet<string> = new Set([
|
|
99
|
+
"slither",
|
|
100
|
+
"manual",
|
|
101
|
+
"pattern",
|
|
102
|
+
"scvd",
|
|
103
|
+
"solodit",
|
|
104
|
+
"fuzz",
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
function normalizeFinding(f: Record<string, unknown>): Finding {
|
|
108
|
+
const severity =
|
|
109
|
+
typeof f.severity === "string" && VALID_SEVERITIES.has(f.severity)
|
|
110
|
+
? (f.severity as Finding["severity"])
|
|
111
|
+
: "Informational"
|
|
112
|
+
const confidence =
|
|
113
|
+
typeof f.confidence === "string" && ["High", "Medium", "Low"].includes(f.confidence)
|
|
114
|
+
? (f.confidence as Finding["confidence"])
|
|
115
|
+
: "Low"
|
|
116
|
+
const source =
|
|
117
|
+
typeof f.source === "string" && VALID_SOURCES.has(f.source)
|
|
118
|
+
? (f.source as Finding["source"])
|
|
119
|
+
: "manual"
|
|
120
|
+
const description = typeof f.description === "string" ? f.description : (f.check as string)
|
|
121
|
+
const id = typeof f.id === "string" ? f.id : `${f.check}:${f.file}:${(f.lines as number[])[0]}`
|
|
122
|
+
return {
|
|
123
|
+
id,
|
|
124
|
+
check: f.check as string,
|
|
125
|
+
severity,
|
|
126
|
+
confidence,
|
|
127
|
+
description,
|
|
128
|
+
file: f.file as string,
|
|
129
|
+
lines: f.lines as [number, number],
|
|
130
|
+
source,
|
|
131
|
+
remediation: typeof f.remediation === "string" ? f.remediation : undefined,
|
|
132
|
+
exploitReference: typeof f.exploitReference === "string" ? f.exploitReference : undefined,
|
|
133
|
+
}
|
|
81
134
|
}
|
|
82
135
|
|
|
83
136
|
export function parseAuditState(auditState: string): AuditState {
|
|
84
|
-
let parsed: unknown
|
|
137
|
+
let parsed: unknown
|
|
85
138
|
try {
|
|
86
|
-
parsed = JSON.parse(auditState)
|
|
139
|
+
parsed = JSON.parse(auditState)
|
|
87
140
|
} catch {
|
|
88
|
-
throw new Error(
|
|
141
|
+
throw new Error(
|
|
142
|
+
"audit_state is not valid JSON — expected an AuditState object or Finding[] array",
|
|
143
|
+
)
|
|
89
144
|
}
|
|
90
145
|
|
|
91
146
|
if (Array.isArray(parsed)) {
|
|
92
|
-
|
|
147
|
+
const validFindings = (parsed as unknown[])
|
|
148
|
+
.filter(hasMinimumFindingFields)
|
|
149
|
+
.map((f) => normalizeFinding(f as Record<string, unknown>))
|
|
150
|
+
return emptyAuditState(validFindings)
|
|
93
151
|
}
|
|
94
152
|
|
|
95
|
-
if (
|
|
96
|
-
|
|
153
|
+
if (
|
|
154
|
+
typeof parsed === "object" &&
|
|
155
|
+
parsed !== null &&
|
|
156
|
+
Array.isArray((parsed as AuditState).findings)
|
|
157
|
+
) {
|
|
158
|
+
const state = parsed as AuditState
|
|
159
|
+
const validFindings = state.findings
|
|
160
|
+
.filter(hasMinimumFindingFields)
|
|
161
|
+
.map((f) => normalizeFinding(f as unknown as Record<string, unknown>))
|
|
97
162
|
return {
|
|
98
163
|
...emptyAuditState(),
|
|
99
164
|
...state,
|
|
100
|
-
|
|
165
|
+
findings: validFindings,
|
|
166
|
+
}
|
|
101
167
|
}
|
|
102
168
|
|
|
103
|
-
return emptyAuditState()
|
|
169
|
+
return emptyAuditState()
|
|
104
170
|
}
|
|
105
171
|
|
|
106
172
|
function normalizeTitle(check: string): string {
|
|
173
|
+
if (!check || typeof check !== "string") return "Unknown Check"
|
|
107
174
|
return check
|
|
108
175
|
.split(/[-_\s]+/)
|
|
109
176
|
.filter((part) => part.length > 0)
|
|
110
177
|
.map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`)
|
|
111
|
-
.join(" ")
|
|
178
|
+
.join(" ")
|
|
112
179
|
}
|
|
113
180
|
|
|
114
181
|
function formatLocation(finding: Finding): string {
|
|
115
|
-
|
|
182
|
+
if (!finding.file || !Array.isArray(finding.lines) || finding.lines.length < 2)
|
|
183
|
+
return "unknown location"
|
|
184
|
+
return `${finding.file}:${finding.lines[0]}-${finding.lines[1]}`
|
|
116
185
|
}
|
|
117
186
|
|
|
118
187
|
function shouldIncludeFinding(finding: Finding, threshold: SeverityThreshold): boolean {
|
|
119
|
-
return FINDING_WEIGHT[finding.severity] >= THRESHOLD_WEIGHT[threshold]
|
|
188
|
+
return FINDING_WEIGHT[finding.severity] >= THRESHOLD_WEIGHT[threshold]
|
|
120
189
|
}
|
|
121
190
|
|
|
122
191
|
function calculateCounts(findings: Finding[]): FindingsCount {
|
|
123
|
-
const counts = emptyCounts()
|
|
192
|
+
const counts = emptyCounts()
|
|
124
193
|
|
|
125
194
|
for (const finding of findings) {
|
|
126
|
-
if (finding.severity === "Critical") counts.critical += 1
|
|
127
|
-
if (finding.severity === "High") counts.high += 1
|
|
128
|
-
if (finding.severity === "Medium") counts.medium += 1
|
|
129
|
-
if (finding.severity === "Low") counts.low += 1
|
|
130
|
-
if (finding.severity === "Informational") counts.informational += 1
|
|
195
|
+
if (finding.severity === "Critical") counts.critical += 1
|
|
196
|
+
if (finding.severity === "High") counts.high += 1
|
|
197
|
+
if (finding.severity === "Medium") counts.medium += 1
|
|
198
|
+
if (finding.severity === "Low") counts.low += 1
|
|
199
|
+
if (finding.severity === "Informational") counts.informational += 1
|
|
131
200
|
}
|
|
132
201
|
|
|
133
|
-
return counts
|
|
202
|
+
return counts
|
|
134
203
|
}
|
|
135
204
|
|
|
136
205
|
function overallRiskAssessment(counts: FindingsCount): string {
|
|
137
|
-
if (counts.critical > 0) return "Critical risk"
|
|
138
|
-
if (counts.high > 0) return "High risk"
|
|
139
|
-
if (counts.medium > 0) return "Medium risk"
|
|
140
|
-
if (counts.low > 0) return "Low risk"
|
|
141
|
-
if (counts.informational > 0) return "Informational only"
|
|
142
|
-
return "No significant risk identified"
|
|
206
|
+
if (counts.critical > 0) return "Critical risk"
|
|
207
|
+
if (counts.high > 0) return "High risk"
|
|
208
|
+
if (counts.medium > 0) return "Medium risk"
|
|
209
|
+
if (counts.low > 0) return "Low risk"
|
|
210
|
+
if (counts.informational > 0) return "Informational only"
|
|
211
|
+
return "No significant risk identified"
|
|
143
212
|
}
|
|
144
213
|
|
|
145
214
|
function genericImpact(severity: FindingSeverity): string {
|
|
146
215
|
if (severity === "Critical") {
|
|
147
|
-
return "Could lead to immediate and severe compromise of funds or protocol control."
|
|
216
|
+
return "Could lead to immediate and severe compromise of funds or protocol control."
|
|
148
217
|
}
|
|
149
218
|
if (severity === "High") {
|
|
150
|
-
return "Could materially impact protocol security, user funds, or system integrity."
|
|
219
|
+
return "Could materially impact protocol security, user funds, or system integrity."
|
|
151
220
|
}
|
|
152
221
|
if (severity === "Medium") {
|
|
153
|
-
return "Could cause operational issues or increase exploitability under specific conditions."
|
|
222
|
+
return "Could cause operational issues or increase exploitability under specific conditions."
|
|
154
223
|
}
|
|
155
224
|
if (severity === "Low") {
|
|
156
|
-
return "Limited direct impact but should be addressed to improve security posture."
|
|
225
|
+
return "Limited direct impact but should be addressed to improve security posture."
|
|
157
226
|
}
|
|
158
|
-
return "No immediate exploit impact, but useful for hardening and maintainability."
|
|
227
|
+
return "No immediate exploit impact, but useful for hardening and maintainability."
|
|
159
228
|
}
|
|
160
229
|
|
|
161
230
|
function genericRecommendation(severity: FindingSeverity): string {
|
|
162
231
|
if (severity === "Critical" || severity === "High") {
|
|
163
|
-
return "Prioritize remediation before production deployment and validate with focused regression tests."
|
|
232
|
+
return "Prioritize remediation before production deployment and validate with focused regression tests."
|
|
164
233
|
}
|
|
165
234
|
if (severity === "Medium") {
|
|
166
|
-
return "Address in the near term and include unit/integration tests to prevent regressions."
|
|
235
|
+
return "Address in the near term and include unit/integration tests to prevent regressions."
|
|
167
236
|
}
|
|
168
237
|
if (severity === "Low") {
|
|
169
|
-
return "Schedule remediation in regular hardening cycles."
|
|
238
|
+
return "Schedule remediation in regular hardening cycles."
|
|
170
239
|
}
|
|
171
|
-
return "Track and resolve during routine code quality and documentation improvements."
|
|
240
|
+
return "Track and resolve during routine code quality and documentation improvements."
|
|
172
241
|
}
|
|
173
242
|
|
|
174
243
|
function buildRecommendations(counts: FindingsCount): string[] {
|
|
175
|
-
const items: string[] = []
|
|
244
|
+
const items: string[] = []
|
|
176
245
|
|
|
177
246
|
if (counts.critical > 0) {
|
|
178
|
-
items.push(
|
|
247
|
+
items.push(
|
|
248
|
+
"1. Immediately remediate all Critical findings and block release until fixes are verified.",
|
|
249
|
+
)
|
|
179
250
|
}
|
|
180
251
|
if (counts.high > 0) {
|
|
181
|
-
items.push(
|
|
252
|
+
items.push(
|
|
253
|
+
"2. Prioritize High findings in the next patch cycle with dedicated security test coverage.",
|
|
254
|
+
)
|
|
182
255
|
}
|
|
183
256
|
if (counts.medium > 0) {
|
|
184
|
-
items.push("3. Resolve Medium findings to reduce attack surface and improve resilience.")
|
|
257
|
+
items.push("3. Resolve Medium findings to reduce attack surface and improve resilience.")
|
|
185
258
|
}
|
|
186
259
|
if (counts.low > 0 || counts.informational > 0) {
|
|
187
|
-
items.push(
|
|
260
|
+
items.push(
|
|
261
|
+
"4. Address Low/Informational findings as part of ongoing hardening and code quality efforts.",
|
|
262
|
+
)
|
|
188
263
|
}
|
|
189
264
|
|
|
190
265
|
if (items.length === 0) {
|
|
191
|
-
items.push(
|
|
266
|
+
items.push(
|
|
267
|
+
"1. Maintain current controls, monitor code changes, and re-audit before major upgrades.",
|
|
268
|
+
)
|
|
192
269
|
}
|
|
193
270
|
|
|
194
|
-
return items
|
|
271
|
+
return items
|
|
195
272
|
}
|
|
196
273
|
|
|
197
274
|
function buildFindingsSection(findings: Finding[]): string {
|
|
198
275
|
if (findings.length === 0) {
|
|
199
|
-
return "## Findings\nNo findings meet the configured severity threshold."
|
|
276
|
+
return "## Findings\nNo findings meet the configured severity threshold."
|
|
200
277
|
}
|
|
201
278
|
|
|
202
|
-
const lines: string[] = ["## Findings"]
|
|
279
|
+
const lines: string[] = ["## Findings"]
|
|
203
280
|
|
|
204
281
|
for (const severity of SEVERITY_ORDER) {
|
|
205
|
-
const severityFindings = findings.filter((finding) => finding.severity === severity)
|
|
282
|
+
const severityFindings = findings.filter((finding) => finding.severity === severity)
|
|
206
283
|
if (severityFindings.length === 0) {
|
|
207
|
-
continue
|
|
284
|
+
continue
|
|
208
285
|
}
|
|
209
286
|
|
|
210
|
-
lines.push(`### ${severity}`)
|
|
287
|
+
lines.push(`### ${severity}`)
|
|
211
288
|
|
|
212
289
|
severityFindings.forEach((finding, index) => {
|
|
213
|
-
const prefix = SEVERITY_PREFIX[severity]
|
|
214
|
-
const findingId = `[${prefix}-${index + 1}]
|
|
215
|
-
const title = normalizeTitle(finding.check)
|
|
216
|
-
const recommendation = finding.remediation ?? genericRecommendation(severity)
|
|
217
|
-
|
|
218
|
-
lines.push(`### ${findingId} ${title}`)
|
|
219
|
-
lines.push(`**Severity**: ${finding.severity}`)
|
|
220
|
-
lines.push(`**Confidence**: ${finding.confidence}`)
|
|
221
|
-
lines.push(`**Location**: ${formatLocation(finding)}`)
|
|
222
|
-
lines.push("")
|
|
223
|
-
lines.push(`**Description**: ${finding.description}`)
|
|
224
|
-
lines.push("")
|
|
225
|
-
lines.push(`**Impact**: ${genericImpact(finding.severity)}`)
|
|
226
|
-
lines.push("")
|
|
227
|
-
lines.push(`**Recommendation**: ${recommendation}`)
|
|
228
|
-
lines.push("")
|
|
229
|
-
})
|
|
290
|
+
const prefix = SEVERITY_PREFIX[severity]
|
|
291
|
+
const findingId = `[${prefix}-${index + 1}]`
|
|
292
|
+
const title = normalizeTitle(finding.check)
|
|
293
|
+
const recommendation = finding.remediation ?? genericRecommendation(severity)
|
|
294
|
+
|
|
295
|
+
lines.push(`### ${findingId} ${title}`)
|
|
296
|
+
lines.push(`**Severity**: ${finding.severity}`)
|
|
297
|
+
lines.push(`**Confidence**: ${finding.confidence}`)
|
|
298
|
+
lines.push(`**Location**: ${formatLocation(finding)}`)
|
|
299
|
+
lines.push("")
|
|
300
|
+
lines.push(`**Description**: ${finding.description}`)
|
|
301
|
+
lines.push("")
|
|
302
|
+
lines.push(`**Impact**: ${genericImpact(finding.severity)}`)
|
|
303
|
+
lines.push("")
|
|
304
|
+
lines.push(`**Recommendation**: ${recommendation}`)
|
|
305
|
+
lines.push("")
|
|
306
|
+
})
|
|
230
307
|
}
|
|
231
308
|
|
|
232
|
-
return lines.join("\n")
|
|
309
|
+
return lines.join("\n")
|
|
233
310
|
}
|
|
234
311
|
|
|
235
312
|
function formatDuration(ms: number): string {
|
|
236
|
-
if (ms < 1000) return `${ms}ms
|
|
237
|
-
return `${(ms / 1000).toFixed(1)}s
|
|
313
|
+
if (ms < 1000) return `${ms}ms`
|
|
314
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
238
315
|
}
|
|
239
316
|
|
|
240
317
|
export function buildProvenanceAppendix(
|
|
@@ -242,174 +319,165 @@ export function buildProvenanceAppendix(
|
|
|
242
319
|
threshold: SeverityThreshold,
|
|
243
320
|
includedCount: number,
|
|
244
321
|
): string {
|
|
245
|
-
const lines: string[] = ["## Appendix: Data Provenance"]
|
|
322
|
+
const lines: string[] = ["## Appendix: Data Provenance"]
|
|
246
323
|
|
|
247
|
-
lines.push("- Data source: `audit_state` payload")
|
|
248
|
-
lines.push(`- Severity threshold applied: ${threshold}`)
|
|
249
|
-
lines.push(`- Findings included in report: ${includedCount}`)
|
|
324
|
+
lines.push("- Data source: `audit_state` payload")
|
|
325
|
+
lines.push(`- Severity threshold applied: ${threshold}`)
|
|
326
|
+
lines.push(`- Findings included in report: ${includedCount}`)
|
|
250
327
|
|
|
251
328
|
if (state.findings.length > 0) {
|
|
252
|
-
const sourceCounts: Record<string, number> = {}
|
|
329
|
+
const sourceCounts: Record<string, number> = {}
|
|
253
330
|
for (const f of state.findings) {
|
|
254
|
-
sourceCounts[f.source] = (sourceCounts[f.source] ?? 0) + 1
|
|
331
|
+
sourceCounts[f.source] = (sourceCounts[f.source] ?? 0) + 1
|
|
255
332
|
}
|
|
256
|
-
lines.push("")
|
|
257
|
-
lines.push("### Source Breakdown")
|
|
258
|
-
lines.push("")
|
|
259
|
-
lines.push("| Source | Count |")
|
|
260
|
-
lines.push("| --- | ---: |")
|
|
261
|
-
for (const [source, count] of Object.entries(sourceCounts).sort(
|
|
262
|
-
(
|
|
263
|
-
)) {
|
|
264
|
-
lines.push(`| ${source} | ${count} |`);
|
|
333
|
+
lines.push("")
|
|
334
|
+
lines.push("### Source Breakdown")
|
|
335
|
+
lines.push("")
|
|
336
|
+
lines.push("| Source | Count |")
|
|
337
|
+
lines.push("| --- | ---: |")
|
|
338
|
+
for (const [source, count] of Object.entries(sourceCounts).sort((a, b) => b[1] - a[1])) {
|
|
339
|
+
lines.push(`| ${source} | ${count} |`)
|
|
265
340
|
}
|
|
266
341
|
}
|
|
267
342
|
|
|
268
343
|
if (state.toolsExecuted.length > 0) {
|
|
269
|
-
lines.push("")
|
|
270
|
-
lines.push("### Tool Execution Summary")
|
|
271
|
-
lines.push("")
|
|
272
|
-
lines.push("| Tool | Duration | Status | Findings |")
|
|
273
|
-
lines.push("| --- | --- | --- | ---: |")
|
|
344
|
+
lines.push("")
|
|
345
|
+
lines.push("### Tool Execution Summary")
|
|
346
|
+
lines.push("")
|
|
347
|
+
lines.push("| Tool | Duration | Status | Findings |")
|
|
348
|
+
lines.push("| --- | --- | --- | ---: |")
|
|
274
349
|
for (const exec of state.toolsExecuted) {
|
|
275
|
-
const duration =
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
: "—";
|
|
279
|
-
const status = exec.success ? "✅ success" : "❌ failure";
|
|
280
|
-
lines.push(
|
|
281
|
-
`| ${exec.tool} | ${duration} | ${status} | ${exec.findingsCount} |`,
|
|
282
|
-
);
|
|
350
|
+
const duration = exec.endTime != null ? formatDuration(exec.endTime - exec.startTime) : "—"
|
|
351
|
+
const status = exec.success ? "✅ success" : "❌ failure"
|
|
352
|
+
lines.push(`| ${exec.tool} | ${duration} | ${status} | ${exec.findingsCount} |`)
|
|
283
353
|
}
|
|
284
354
|
}
|
|
285
355
|
|
|
286
|
-
const syncExec = state.toolsExecuted.find((t) => t.tool === "argus_sync_knowledge")
|
|
356
|
+
const syncExec = state.toolsExecuted.find((t) => t.tool === "argus_sync_knowledge")
|
|
287
357
|
if (state.patternVersion || syncExec) {
|
|
288
|
-
lines.push("")
|
|
289
|
-
lines.push("### Data Freshness")
|
|
290
|
-
lines.push("")
|
|
358
|
+
lines.push("")
|
|
359
|
+
lines.push("### Data Freshness")
|
|
360
|
+
lines.push("")
|
|
291
361
|
if (state.patternVersion) {
|
|
292
|
-
lines.push(`- Pattern pack version: \`${state.patternVersion}\``)
|
|
362
|
+
lines.push(`- Pattern pack version: \`${state.patternVersion}\``)
|
|
293
363
|
}
|
|
294
364
|
if (syncExec) {
|
|
295
|
-
lines.push(`- SCVD last synced: ${new Date(syncExec.startTime).toISOString()}`)
|
|
365
|
+
lines.push(`- SCVD last synced: ${new Date(syncExec.startTime).toISOString()}`)
|
|
296
366
|
}
|
|
297
367
|
}
|
|
298
368
|
|
|
299
369
|
if (state.soloditResults && state.soloditResults.length > 0) {
|
|
300
|
-
lines.push("")
|
|
301
|
-
lines.push("### Solodit Cross-References")
|
|
302
|
-
lines.push("")
|
|
370
|
+
lines.push("")
|
|
371
|
+
lines.push("### Solodit Cross-References")
|
|
372
|
+
lines.push("")
|
|
303
373
|
for (const result of state.soloditResults) {
|
|
304
|
-
lines.push(`**Query**: "${result.query}" — ${result.resultCount} results`)
|
|
374
|
+
lines.push(`**Query**: "${result.query}" — ${result.resultCount} results`)
|
|
305
375
|
if (result.topResults.length > 0) {
|
|
306
|
-
lines.push("")
|
|
307
|
-
lines.push("| Title | Severity | Protocol |")
|
|
308
|
-
lines.push("| --- | --- | --- |")
|
|
376
|
+
lines.push("")
|
|
377
|
+
lines.push("| Title | Severity | Protocol |")
|
|
378
|
+
lines.push("| --- | --- | --- |")
|
|
309
379
|
for (const top of result.topResults) {
|
|
310
|
-
lines.push(`| ${top.title} | ${top.severity} | ${top.protocol} |`)
|
|
380
|
+
lines.push(`| ${top.title} | ${top.severity} | ${top.protocol} |`)
|
|
311
381
|
}
|
|
312
382
|
}
|
|
313
|
-
lines.push("")
|
|
383
|
+
lines.push("")
|
|
314
384
|
}
|
|
315
385
|
}
|
|
316
386
|
|
|
317
387
|
if (state.fuzzCounterexamples && state.fuzzCounterexamples.length > 0) {
|
|
318
|
-
lines.push("")
|
|
319
|
-
lines.push("### Fuzz Evidence")
|
|
320
|
-
lines.push("")
|
|
321
|
-
lines.push("| Test | Inputs | Runs | Revert Reason |")
|
|
322
|
-
lines.push("| --- | --- | ---: | --- |")
|
|
388
|
+
lines.push("")
|
|
389
|
+
lines.push("### Fuzz Evidence")
|
|
390
|
+
lines.push("")
|
|
391
|
+
lines.push("| Test | Inputs | Runs | Revert Reason |")
|
|
392
|
+
lines.push("| --- | --- | ---: | --- |")
|
|
323
393
|
for (const cx of state.fuzzCounterexamples) {
|
|
324
|
-
const inputs = cx.inputs.join(", ")
|
|
325
|
-
const reason = cx.revertReason ?? "—"
|
|
326
|
-
lines.push(`| ${cx.testName} | ${inputs} | ${cx.runs} | ${reason} |`)
|
|
394
|
+
const inputs = cx.inputs.join(", ")
|
|
395
|
+
const reason = cx.revertReason ?? "—"
|
|
396
|
+
lines.push(`| ${cx.testName} | ${inputs} | ${cx.runs} | ${reason} |`)
|
|
327
397
|
}
|
|
328
398
|
}
|
|
329
399
|
|
|
330
400
|
if (state.skillsLoaded && state.skillsLoaded.length > 0) {
|
|
331
|
-
lines.push("")
|
|
332
|
-
lines.push("### Knowledge Sources")
|
|
333
|
-
lines.push("")
|
|
334
|
-
lines.push("Skills loaded during this audit:")
|
|
335
|
-
lines.push("")
|
|
401
|
+
lines.push("")
|
|
402
|
+
lines.push("### Knowledge Sources")
|
|
403
|
+
lines.push("")
|
|
404
|
+
lines.push("Skills loaded during this audit:")
|
|
405
|
+
lines.push("")
|
|
336
406
|
for (const skill of state.skillsLoaded) {
|
|
337
|
-
lines.push(`- ${skill}`)
|
|
407
|
+
lines.push(`- ${skill}`)
|
|
338
408
|
}
|
|
339
409
|
}
|
|
340
410
|
|
|
341
|
-
return lines.join("\n")
|
|
411
|
+
return lines.join("\n")
|
|
342
412
|
}
|
|
343
413
|
|
|
344
414
|
export async function executeReportGeneration(
|
|
345
415
|
args: ReportGeneratorArgs,
|
|
346
|
-
context: ToolContext
|
|
416
|
+
context: ToolContext,
|
|
347
417
|
): Promise<ReportGenerationResult> {
|
|
348
|
-
const includeExecutiveSummary = args.include_executive_summary ?? true
|
|
349
|
-
const threshold = args.severity_threshold ?? "low"
|
|
350
|
-
const state = parseAuditState(args.audit_state)
|
|
351
|
-
const findings = state.findings.filter((finding) =>
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
const counts = calculateCounts(findings);
|
|
355
|
-
const auditDate = new Date().toISOString().slice(0, 10);
|
|
418
|
+
const includeExecutiveSummary = args.include_executive_summary ?? true
|
|
419
|
+
const threshold = args.severity_threshold ?? "low"
|
|
420
|
+
const state = parseAuditState(args.audit_state)
|
|
421
|
+
const findings = state.findings.filter((finding) => shouldIncludeFinding(finding, threshold))
|
|
422
|
+
const counts = calculateCounts(findings)
|
|
423
|
+
const auditDate = new Date().toISOString().slice(0, 10)
|
|
356
424
|
|
|
357
|
-
context.metadata({ title: `Generate audit report: ${args.project_name}` })
|
|
425
|
+
context.metadata({ title: `Generate audit report: ${args.project_name}` })
|
|
358
426
|
|
|
359
|
-
const sections: string[] = [`# Security Audit Report — ${args.project_name}`]
|
|
427
|
+
const sections: string[] = [`# Security Audit Report — ${args.project_name}`]
|
|
360
428
|
|
|
361
429
|
if (includeExecutiveSummary) {
|
|
362
|
-
sections.push("## Executive Summary")
|
|
430
|
+
sections.push("## Executive Summary")
|
|
363
431
|
sections.push(
|
|
364
|
-
`This report summarizes security findings identified for ${args.project_name} based on static analysis, testing, and pattern-based review
|
|
365
|
-
)
|
|
366
|
-
sections.push("")
|
|
367
|
-
sections.push("| Severity | Count |")
|
|
368
|
-
sections.push("| --- | ---: |")
|
|
369
|
-
sections.push(`| Critical | ${counts.critical} |`)
|
|
370
|
-
sections.push(`| High | ${counts.high} |`)
|
|
371
|
-
sections.push(`| Medium | ${counts.medium} |`)
|
|
372
|
-
sections.push(`| Low | ${counts.low} |`)
|
|
373
|
-
sections.push(`| Informational | ${counts.informational} |`)
|
|
374
|
-
sections.push("")
|
|
375
|
-
sections.push(`Overall risk assessment: ${overallRiskAssessment(counts)}.`)
|
|
432
|
+
`This report summarizes security findings identified for ${args.project_name} based on static analysis, testing, and pattern-based review.`,
|
|
433
|
+
)
|
|
434
|
+
sections.push("")
|
|
435
|
+
sections.push("| Severity | Count |")
|
|
436
|
+
sections.push("| --- | ---: |")
|
|
437
|
+
sections.push(`| Critical | ${counts.critical} |`)
|
|
438
|
+
sections.push(`| High | ${counts.high} |`)
|
|
439
|
+
sections.push(`| Medium | ${counts.medium} |`)
|
|
440
|
+
sections.push(`| Low | ${counts.low} |`)
|
|
441
|
+
sections.push(`| Informational | ${counts.informational} |`)
|
|
442
|
+
sections.push("")
|
|
443
|
+
sections.push(`Overall risk assessment: ${overallRiskAssessment(counts)}.`)
|
|
376
444
|
}
|
|
377
445
|
|
|
378
|
-
sections.push("## Scope")
|
|
379
|
-
sections.push("Contracts in scope:")
|
|
446
|
+
sections.push("## Scope")
|
|
447
|
+
sections.push("Contracts in scope:")
|
|
380
448
|
if (args.scope.length === 0) {
|
|
381
|
-
sections.push("- None provided")
|
|
449
|
+
sections.push("- None provided")
|
|
382
450
|
} else {
|
|
383
451
|
for (const contract of args.scope) {
|
|
384
|
-
sections.push(`- ${contract}`)
|
|
452
|
+
sections.push(`- ${contract}`)
|
|
385
453
|
}
|
|
386
454
|
}
|
|
387
|
-
sections.push(`Audit date: ${auditDate}`)
|
|
388
|
-
|
|
389
|
-
sections.push("## Methodology")
|
|
390
|
-
sections.push("Tools and techniques used:")
|
|
391
|
-
sections.push("- Slither static analysis")
|
|
392
|
-
sections.push("- Foundry tests and fuzzing")
|
|
393
|
-
sections.push("- Pattern Analysis")
|
|
394
|
-
sections.push("- Solodit research cross-referencing")
|
|
455
|
+
sections.push(`Audit date: ${auditDate}`)
|
|
456
|
+
|
|
457
|
+
sections.push("## Methodology")
|
|
458
|
+
sections.push("Tools and techniques used:")
|
|
459
|
+
sections.push("- Slither static analysis")
|
|
460
|
+
sections.push("- Foundry tests and fuzzing")
|
|
461
|
+
sections.push("- Pattern Analysis")
|
|
462
|
+
sections.push("- Solodit research cross-referencing")
|
|
395
463
|
sections.push(
|
|
396
|
-
"Approach: Findings were normalized, deduplicated by detector signature and location, then prioritized by severity and confidence."
|
|
397
|
-
)
|
|
464
|
+
"Approach: Findings were normalized, deduplicated by detector signature and location, then prioritized by severity and confidence.",
|
|
465
|
+
)
|
|
398
466
|
|
|
399
|
-
sections.push(buildFindingsSection(findings))
|
|
467
|
+
sections.push(buildFindingsSection(findings))
|
|
400
468
|
|
|
401
|
-
sections.push("## Recommendations")
|
|
469
|
+
sections.push("## Recommendations")
|
|
402
470
|
for (const item of buildRecommendations(counts)) {
|
|
403
|
-
sections.push(`- ${item}`)
|
|
471
|
+
sections.push(`- ${item}`)
|
|
404
472
|
}
|
|
405
473
|
|
|
406
|
-
sections.push(buildProvenanceAppendix(state, threshold, findings.length))
|
|
474
|
+
sections.push(buildProvenanceAppendix(state, threshold, findings.length))
|
|
407
475
|
|
|
408
476
|
return {
|
|
409
477
|
report: sections.join("\n\n"),
|
|
410
478
|
findingsCount: counts,
|
|
411
479
|
filename: `${args.project_name}-audit-report-${auditDate}.md`,
|
|
412
|
-
}
|
|
480
|
+
}
|
|
413
481
|
}
|
|
414
482
|
|
|
415
483
|
export const reportGeneratorTool = tool({
|
|
@@ -425,7 +493,7 @@ export const reportGeneratorTool = tool({
|
|
|
425
493
|
audit_state: tool.schema.string(),
|
|
426
494
|
},
|
|
427
495
|
async execute(args, context) {
|
|
428
|
-
const result = await executeReportGeneration(args, context)
|
|
429
|
-
return JSON.stringify(result)
|
|
496
|
+
const result = await executeReportGeneration(args, context)
|
|
497
|
+
return JSON.stringify(result)
|
|
430
498
|
},
|
|
431
|
-
})
|
|
499
|
+
})
|