solidity-argus 0.3.6 → 0.5.6
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 +13 -6
- package/README.md +24 -12
- package/package.json +7 -3
- package/skills/checklists/cyfrin-best-practices-runtime/SKILL.md +1 -0
- package/skills/checklists/cyfrin-best-practices-upgrades/SKILL.md +1 -0
- package/skills/checklists/cyfrin-defi-core/SKILL.md +1 -0
- package/skills/checklists/cyfrin-defi-integrations/SKILL.md +1 -0
- package/skills/checklists/cyfrin-gas/SKILL.md +1 -0
- package/skills/checklists/general-audit/SKILL.md +1 -0
- package/skills/methodology/audit-workflow/SKILL.md +1 -0
- package/skills/methodology/report-template/SKILL.md +1 -0
- package/skills/methodology/severity-classification/SKILL.md +1 -0
- package/skills/protocol-patterns/amm-dex/SKILL.md +1 -0
- package/skills/protocol-patterns/bridges-cross-chain/SKILL.md +1 -0
- package/skills/protocol-patterns/dao-governance/SKILL.md +1 -0
- package/skills/protocol-patterns/lending-borrowing/SKILL.md +1 -0
- package/skills/protocol-patterns/staking-vesting/SKILL.md +1 -0
- package/skills/vulnerability-patterns/flash-loan-attacks/SKILL.md +0 -50
- package/skills/vulnerability-patterns/oracle-manipulation/SKILL.md +0 -63
- package/src/agents/argus-prompt.ts +98 -33
- package/src/agents/pythia-prompt.ts +18 -1
- package/src/agents/scribe-prompt.ts +32 -10
- package/src/agents/sentinel-prompt.ts +19 -0
- package/src/agents/themis-prompt.ts +110 -0
- package/src/cli/commands/doctor.ts +29 -17
- package/src/config/loader.ts +29 -5
- package/src/config/schema.ts +45 -45
- package/src/constants/defaults.ts +1 -0
- package/src/create-hooks.ts +851 -142
- package/src/create-managers.ts +4 -2
- package/src/create-tools.ts +5 -1
- package/src/features/audit-enforcer/audit-enforcer.ts +1 -11
- package/src/features/background-agent/background-manager.ts +32 -5
- package/src/features/error-recovery/tool-error-recovery.ts +1 -0
- package/src/features/persistent-state/audit-state-manager.ts +272 -29
- package/src/features/persistent-state/event-sink.ts +96 -25
- package/src/features/persistent-state/findings-materializer.ts +57 -3
- package/src/features/persistent-state/global-run-index.ts +86 -8
- package/src/features/persistent-state/index.ts +7 -1
- package/src/features/persistent-state/run-finalizer.ts +116 -7
- package/src/features/persistent-state/run-pruner.ts +93 -0
- package/src/hooks/agent-tracker.ts +14 -2
- package/src/hooks/compaction-hook.ts +7 -16
- package/src/hooks/config-handler.ts +83 -29
- package/src/hooks/context-budget.ts +4 -5
- package/src/hooks/event-hook.ts +213 -57
- package/src/hooks/knowledge-sync-hook.ts +2 -3
- package/src/hooks/safe-create-hook.ts +13 -1
- package/src/hooks/system-prompt-hook.ts +20 -39
- package/src/hooks/tool-tracking-hook.ts +606 -326
- package/src/index.ts +15 -1
- package/src/knowledge/scvd-client.ts +2 -4
- package/src/knowledge/scvd-errors.ts +25 -2
- package/src/knowledge/scvd-index.ts +7 -5
- package/src/knowledge/scvd-sync.ts +6 -6
- package/src/managers/types.ts +20 -2
- package/src/shared/agent-names.ts +23 -0
- package/src/shared/audit-artifact-resolver.ts +8 -3
- package/src/shared/audit-phases.ts +12 -0
- package/src/shared/cache-paths.ts +41 -0
- package/src/shared/drop-diagnostics.ts +2 -2
- package/src/shared/forge-errors.ts +31 -0
- package/src/shared/forge-runner.ts +30 -0
- package/src/shared/format-error.ts +3 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/key-tools.ts +39 -0
- package/src/shared/logger.ts +7 -7
- package/src/shared/path-containment.ts +25 -0
- package/src/shared/path-utils.ts +11 -0
- package/src/shared/report-path-resolver.ts +4 -2
- package/src/shared/safe-emit.ts +24 -0
- package/src/shared/token-utils.ts +5 -0
- package/src/shared/type-guards.ts +8 -0
- package/src/shared/validation-constants.ts +52 -0
- package/src/skills/analysis/cluster.ts +1 -114
- package/src/skills/analysis/normalize.ts +2 -114
- package/src/skills/analysis/stopwords.ts +109 -0
- package/src/skills/argus-skill-resolver.ts +6 -3
- package/src/solodit-lifecycle.ts +153 -37
- package/src/state/adapters.ts +60 -66
- package/src/state/finding-aggregation.ts +6 -8
- package/src/state/finding-fingerprint.ts +1 -1
- package/src/state/finding-store.ts +31 -9
- package/src/state/index.ts +1 -1
- package/src/state/projectors.ts +27 -19
- package/src/state/schemas.ts +8 -32
- package/src/state/types.ts +3 -0
- package/src/tools/contract-analyzer-tool.ts +4 -6
- package/src/tools/forge-coverage-tool.ts +10 -35
- package/src/tools/forge-fuzz-tool.ts +21 -51
- package/src/tools/forge-test-tool.ts +25 -47
- package/src/tools/gas-analysis-tool.ts +12 -41
- package/src/tools/pattern-checker-tool.ts +37 -15
- package/src/tools/pattern-loader.ts +18 -4
- package/src/tools/persist-deduped-tool.ts +94 -0
- package/src/tools/proxy-detection-tool.ts +35 -34
- package/src/tools/read-findings-tool.ts +390 -0
- package/src/tools/record-finding-tool.ts +120 -25
- package/src/tools/report-generator-tool.ts +396 -328
- package/src/tools/report-preflight.ts +5 -1
- package/src/tools/slither-tool.ts +55 -16
- package/src/tools/solodit-search-tool.ts +260 -112
- package/src/tools/sync-knowledge-tool.ts +2 -3
- package/src/utils/solidity-parser.ts +39 -24
- package/src/features/migration/index.ts +0 -14
- package/src/features/migration/migration-adapter.ts +0 -151
- package/src/features/migration/parity-telemetry.ts +0 -133
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from "node:fs"
|
|
2
|
-
import
|
|
3
|
-
import { dirname, extname, join, resolve } from "node:path"
|
|
2
|
+
import { dirname, extname, isAbsolute, join, resolve } from "node:path"
|
|
4
3
|
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
5
4
|
import {
|
|
6
5
|
loadIndex,
|
|
@@ -8,7 +7,10 @@ import {
|
|
|
8
7
|
type ScvdIndexEntry,
|
|
9
8
|
searchIndex,
|
|
10
9
|
} from "../knowledge/scvd-index"
|
|
10
|
+
import { getScvdIndexPath } from "../shared/cache-paths"
|
|
11
11
|
import { createLogger } from "../shared/logger"
|
|
12
|
+
import { normalizeFilePath } from "../shared/path-utils"
|
|
13
|
+
import { resolveProjectDir } from "../shared/project-utils"
|
|
12
14
|
import { extractDetectionRulesFromSkills } from "./pattern-loader"
|
|
13
15
|
import type { PatternDefinition } from "./pattern-schema"
|
|
14
16
|
|
|
@@ -153,7 +155,7 @@ async function collectScvdMatches(
|
|
|
153
155
|
return []
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
const indexPath =
|
|
158
|
+
const indexPath = getScvdIndexPath()
|
|
157
159
|
const index = await dependencies.loadIndexFn(indexPath)
|
|
158
160
|
|
|
159
161
|
if (!index) {
|
|
@@ -241,20 +243,24 @@ function lineWindow(content: string, index: number): [number, number] {
|
|
|
241
243
|
return [start, end]
|
|
242
244
|
}
|
|
243
245
|
|
|
244
|
-
export function findMatches(file: string, patterns: LoadedPattern[]): Match[] {
|
|
246
|
+
export function findMatches(file: string, patterns: LoadedPattern[], projectDir?: string): Match[] {
|
|
245
247
|
const content = readFileSync(file, "utf8")
|
|
248
|
+
const normalizedFile = projectDir ? normalizeFilePath(file, projectDir) : file
|
|
246
249
|
const matches: Match[] = []
|
|
247
250
|
|
|
248
251
|
// Strip comments and string literals to reduce false positives.
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
const stripped = content
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
252
|
+
// Single-pass approach: match whichever construct appears first so that
|
|
253
|
+
// a "//" inside a string (e.g. URLs) is NOT treated as a comment.
|
|
254
|
+
const stripped = content.replace(
|
|
255
|
+
/\/\*[\s\S]*?\*\/|\/\/[^\n]*|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'/g,
|
|
256
|
+
(m) => {
|
|
257
|
+
if (m.startsWith("/*")) return m.replace(/[^\n]/g, " ")
|
|
258
|
+
if (m.startsWith("//")) return " ".repeat(m.length)
|
|
259
|
+
// String literal — preserve quotes, blank interior
|
|
255
260
|
const quote = m[0]
|
|
256
261
|
return `${quote}${" ".repeat(Math.max(0, m.length - 2))}${quote}`
|
|
257
|
-
}
|
|
262
|
+
},
|
|
263
|
+
)
|
|
258
264
|
|
|
259
265
|
for (const pattern of patterns) {
|
|
260
266
|
const regex = new RegExp(
|
|
@@ -266,7 +272,7 @@ export function findMatches(file: string, patterns: LoadedPattern[]): Match[] {
|
|
|
266
272
|
matches.push({
|
|
267
273
|
pattern: pattern.name,
|
|
268
274
|
severity: pattern.severity,
|
|
269
|
-
file,
|
|
275
|
+
file: normalizedFile,
|
|
270
276
|
lines: lineWindow(content, index),
|
|
271
277
|
description: pattern.description,
|
|
272
278
|
exploitReference: pattern.exploitReference,
|
|
@@ -306,14 +312,24 @@ export async function executePatternCheck(
|
|
|
306
312
|
context.metadata({ title: `Pattern check: ${args.target}` })
|
|
307
313
|
|
|
308
314
|
const skillsDir = join(dirname(dirname(__dirname)), "skills")
|
|
309
|
-
const skillDetectionRules =
|
|
315
|
+
const { patterns: skillDetectionRules, errors: loaderErrors } =
|
|
316
|
+
extractDetectionRulesFromSkills(skillsDir)
|
|
317
|
+
if (loaderErrors.length > 0) {
|
|
318
|
+
for (const err of loaderErrors) {
|
|
319
|
+
logger.warn(`Pattern loader: ${err}`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
310
322
|
|
|
311
323
|
const allPatterns: LoadedPattern[] = [
|
|
312
324
|
...normalizePatternDefinitions(skillDetectionRules, "skill"),
|
|
313
325
|
]
|
|
314
326
|
|
|
315
327
|
const selectedPatterns = selectPatterns(allPatterns, args.patterns)
|
|
316
|
-
const
|
|
328
|
+
const baseProjectDir = resolveProjectDir(context)
|
|
329
|
+
const resolvedTarget = isAbsolute(args.target)
|
|
330
|
+
? args.target
|
|
331
|
+
: resolve(baseProjectDir, args.target)
|
|
332
|
+
const solidityFiles = collectSolidityFiles(resolvedTarget)
|
|
317
333
|
if (solidityFiles.length === 0) {
|
|
318
334
|
return {
|
|
319
335
|
success: false,
|
|
@@ -328,12 +344,18 @@ export async function executePatternCheck(
|
|
|
328
344
|
}
|
|
329
345
|
}
|
|
330
346
|
|
|
347
|
+
const absoluteTarget = resolvedTarget
|
|
348
|
+
let targetStat: ReturnType<typeof statSync> | undefined
|
|
349
|
+
try {
|
|
350
|
+
targetStat = statSync(absoluteTarget)
|
|
351
|
+
} catch {}
|
|
352
|
+
const normalizedProjectDir = targetStat?.isFile() ? dirname(absoluteTarget) : absoluteTarget
|
|
331
353
|
const sourceMatches: Match[] = []
|
|
332
354
|
for (const solidityFile of solidityFiles) {
|
|
333
355
|
if (context.abort.aborted) {
|
|
334
356
|
throw new Error("pattern check aborted")
|
|
335
357
|
}
|
|
336
|
-
sourceMatches.push(...findMatches(solidityFile, selectedPatterns))
|
|
358
|
+
sourceMatches.push(...findMatches(solidityFile, selectedPatterns, normalizedProjectDir))
|
|
337
359
|
}
|
|
338
360
|
|
|
339
361
|
const sources: MatchSource[] = [
|
|
@@ -36,9 +36,15 @@ function listSkillMarkdownFiles(skillsDir: string): string[] {
|
|
|
36
36
|
return files
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export
|
|
39
|
+
export interface PatternLoaderResult {
|
|
40
|
+
patterns: PatternDefinition[]
|
|
41
|
+
errors: string[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function extractDetectionRulesFromSkills(skillsDir: string): PatternLoaderResult {
|
|
40
45
|
const skillFiles = listSkillMarkdownFiles(skillsDir)
|
|
41
46
|
const extracted: PatternDefinition[] = []
|
|
47
|
+
const errors: string[] = []
|
|
42
48
|
|
|
43
49
|
for (const filePath of skillFiles) {
|
|
44
50
|
try {
|
|
@@ -47,7 +53,13 @@ export function extractDetectionRulesFromSkills(skillsDir: string): PatternDefin
|
|
|
47
53
|
if (!frontmatter) continue
|
|
48
54
|
|
|
49
55
|
const parsed = SkillFrontmatterSchema.safeParse(frontmatter)
|
|
50
|
-
if (!parsed.success)
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
const reason = parsed.error.issues.map((i) => i.message).join("; ")
|
|
58
|
+
const msg = `Failed to parse ${filePath}: ${reason}`
|
|
59
|
+
logger.warn(msg)
|
|
60
|
+
errors.push(msg)
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
51
63
|
|
|
52
64
|
const skillName = parsed.data.name
|
|
53
65
|
const category = parsed.data.pattern_category
|
|
@@ -69,9 +81,11 @@ export function extractDetectionRulesFromSkills(skillsDir: string): PatternDefin
|
|
|
69
81
|
})
|
|
70
82
|
}
|
|
71
83
|
} catch (err) {
|
|
72
|
-
|
|
84
|
+
const msg = `Failed to parse ${filePath}: ${err instanceof Error ? err.message : "parse error"}`
|
|
85
|
+
logger.warn(msg)
|
|
86
|
+
errors.push(msg)
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
return extracted
|
|
90
|
+
return { patterns: extracted, errors }
|
|
77
91
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
|
2
|
+
import { dirname } from "node:path"
|
|
3
|
+
import { type ToolContext, tool } from "@opencode-ai/plugin"
|
|
4
|
+
import { createAuditArtifactResolver } from "../shared/audit-artifact-resolver"
|
|
5
|
+
import { createLogger } from "../shared/logger"
|
|
6
|
+
import { resolveProjectDir } from "../shared/project-utils"
|
|
7
|
+
import { isNonEmptyString } from "../shared/type-guards"
|
|
8
|
+
import type { CanonicalFinding } from "../state/schemas"
|
|
9
|
+
import { SCHEMA_VERSION } from "../state/schemas"
|
|
10
|
+
|
|
11
|
+
type PersistDedupedArgs = {
|
|
12
|
+
run_id: string
|
|
13
|
+
deduped_findings: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DedupedFindingsArtifact {
|
|
17
|
+
run_id: string
|
|
18
|
+
schema_version: string
|
|
19
|
+
deduped_at: number
|
|
20
|
+
deduped_by: string
|
|
21
|
+
findings_count: number
|
|
22
|
+
findings: CanonicalFinding[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function executePersistDeduped(
|
|
26
|
+
args: PersistDedupedArgs,
|
|
27
|
+
context: ToolContext,
|
|
28
|
+
): Promise<string> {
|
|
29
|
+
const logger = createLogger()
|
|
30
|
+
|
|
31
|
+
if (!isNonEmptyString(args.run_id)) {
|
|
32
|
+
return JSON.stringify({ success: false, error: "run_id is required" })
|
|
33
|
+
}
|
|
34
|
+
if (!isNonEmptyString(args.deduped_findings)) {
|
|
35
|
+
return JSON.stringify({ success: false, error: "deduped_findings is required" })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let findings: CanonicalFinding[]
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(args.deduped_findings)
|
|
41
|
+
findings = Array.isArray(parsed) ? parsed : parsed.findings
|
|
42
|
+
if (!Array.isArray(findings)) {
|
|
43
|
+
return JSON.stringify({
|
|
44
|
+
success: false,
|
|
45
|
+
error: "deduped_findings must be a JSON array or an object with a findings array",
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
success: false,
|
|
51
|
+
error: `Invalid JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const projectDir = resolveProjectDir(context)
|
|
56
|
+
const resolver = createAuditArtifactResolver(args.run_id, projectDir)
|
|
57
|
+
const dedupedPath = resolver.paths().dedupedFindingsFile
|
|
58
|
+
|
|
59
|
+
const artifact: DedupedFindingsArtifact = {
|
|
60
|
+
run_id: args.run_id,
|
|
61
|
+
schema_version: SCHEMA_VERSION,
|
|
62
|
+
deduped_at: Date.now(),
|
|
63
|
+
deduped_by: context.agent ?? "scribe",
|
|
64
|
+
findings_count: findings.length,
|
|
65
|
+
findings,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
await mkdir(dirname(dedupedPath), { recursive: true })
|
|
69
|
+
await writeFile(dedupedPath, JSON.stringify(artifact, null, 2))
|
|
70
|
+
logger.debug(`Persisted ${findings.length} deduped findings to ${dedupedPath}`)
|
|
71
|
+
|
|
72
|
+
return JSON.stringify({
|
|
73
|
+
success: true,
|
|
74
|
+
path: dedupedPath,
|
|
75
|
+
findings_count: findings.length,
|
|
76
|
+
schema_version: SCHEMA_VERSION,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const persistDedupedTool = tool({
|
|
81
|
+
description:
|
|
82
|
+
"Persist deduplicated and enriched findings to disk as the source-of-truth JSON artifact. Call this BEFORE argus_generate_report so the report tool can read from disk instead of requiring inline data.",
|
|
83
|
+
args: {
|
|
84
|
+
run_id: tool.schema.string().describe("The canonical run ID from <argus-context>."),
|
|
85
|
+
deduped_findings: tool.schema
|
|
86
|
+
.string()
|
|
87
|
+
.describe(
|
|
88
|
+
"Serialized JSON array of deduplicated and enriched findings. Each finding should have: check, severity, confidence, description, file, lines, source, impact, recommendation.",
|
|
89
|
+
),
|
|
90
|
+
},
|
|
91
|
+
async execute(args, context) {
|
|
92
|
+
return executePersistDeduped(args, context)
|
|
93
|
+
},
|
|
94
|
+
})
|
|
@@ -115,49 +115,50 @@ function collectIndicators(source: string): Set<string> {
|
|
|
115
115
|
return indicators
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (hasAny(indicators, ["beacon-interface", "beacon-proxy", "upgradeable-beacon"])) {
|
|
136
|
-
return "beacon"
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
hasAny(indicators, [
|
|
118
|
+
// Scored classification: each proxy type gets a score based on how many
|
|
119
|
+
// of its specific indicators are present. The type with the highest score
|
|
120
|
+
// wins. This avoids implicit order-dependency when multiple proxy types
|
|
121
|
+
// have matching indicators.
|
|
122
|
+
const PROXY_TYPE_INDICATORS: Array<{ type: ProxyType; indicators: string[] }> = [
|
|
123
|
+
{
|
|
124
|
+
type: "diamond",
|
|
125
|
+
indicators: ["diamond-cut", "diamond-cut-interface", "facet-address", "diamond-loupe"],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: "uups",
|
|
129
|
+
indicators: ["uups-authorize-upgrade", "uups-upgrade-to-and-call", "uups-upgradeable"],
|
|
130
|
+
},
|
|
131
|
+
{ type: "beacon", indicators: ["beacon-interface", "beacon-proxy", "upgradeable-beacon"] },
|
|
132
|
+
{
|
|
133
|
+
type: "transparent",
|
|
134
|
+
indicators: [
|
|
141
135
|
"transparent-implementation-getter",
|
|
142
136
|
"transparent-admin-getter",
|
|
143
137
|
"transparent-set-implementation",
|
|
144
|
-
]
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
hasAny(indicators, [
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: "erc1967",
|
|
142
|
+
indicators: [
|
|
151
143
|
"erc1967-implementation-slot",
|
|
152
144
|
"erc1967-admin-slot",
|
|
153
145
|
"erc1967-beacon-slot",
|
|
154
146
|
"delegatecall",
|
|
155
|
-
]
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
function classifyProxyType(indicators: Set<string>): ProxyType | null {
|
|
152
|
+
let best: { type: ProxyType; score: number } | null = null
|
|
153
|
+
|
|
154
|
+
for (const entry of PROXY_TYPE_INDICATORS) {
|
|
155
|
+
const score = entry.indicators.filter((ind) => indicators.has(ind)).length
|
|
156
|
+
if (score > 0 && (!best || score > best.score)) {
|
|
157
|
+
best = { type: entry.type, score }
|
|
158
|
+
}
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
return null
|
|
161
|
+
return best?.type ?? null
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
export async function executeProxyDetection(
|