@gatedpath/preflight 0.2.3 → 0.2.4
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/dist/checks/slither.d.ts +10 -0
- package/dist/checks/slither.js +65 -3
- package/package.json +3 -3
package/dist/checks/slither.d.ts
CHANGED
|
@@ -9,3 +9,13 @@ export declare const SOURCES_HASH_FILE = "slither.sources.sha256";
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function hashSources(dir: string): string;
|
|
11
11
|
export declare function checkSlitherReport(project: string, sourceDir: string, reportPath?: string): Promise<CheckResult>;
|
|
12
|
+
/** The project's `fail_on` from slither.config.json, or Slither's default, medium. */
|
|
13
|
+
export declare function readFailOn(project: string): string;
|
|
14
|
+
export interface SlitherFinding {
|
|
15
|
+
check: string;
|
|
16
|
+
impact: string;
|
|
17
|
+
confidence: string;
|
|
18
|
+
where: string;
|
|
19
|
+
}
|
|
20
|
+
/** Every detector result in a Slither JSON report, with one location each. */
|
|
21
|
+
export declare function readFindings(reportFile: string): SlitherFinding[] | null;
|
package/dist/checks/slither.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Does a Slither report exist,
|
|
1
|
+
// Does a Slither report exist, does it describe the sources as they are now, and is it clean at the
|
|
2
|
+
// project's own threshold? Freshness first, two ways to
|
|
2
3
|
// answer. Preferred: a sidecar `slither.sources.sha256` next to the report holding the hash
|
|
3
4
|
// of every .sol file under the source directory at the time Slither ran (the scaffold's
|
|
4
5
|
// `npm run lint:slither` writes it); the check recomputes the hash and compares. Fallback,
|
|
@@ -8,6 +9,12 @@
|
|
|
8
9
|
// time rule cannot be satisfied by re-running Slither on a clean project, because a report
|
|
9
10
|
// with no findings is byte-identical and git records no new change for it (found on
|
|
10
11
|
// 2026-09-12 running the prompt cards), which is why the hash sidecar exists.
|
|
12
|
+
// Then the findings: a fresh report is not a clean one. Until 29 October 2026 this check stopped at
|
|
13
|
+
// freshness, and a ship report said "pass" over a report holding a High (found on the first public CI
|
|
14
|
+
// run of grant-milestone-escrow, where the same report failed `fail-on: medium`). A JSON report's
|
|
15
|
+
// detectors are now read and any finding at or above the project's `fail_on` in slither.config.json
|
|
16
|
+
// (default medium, Slither's own default) fails the check, naming them. A markdown report cannot be
|
|
17
|
+
// read; the check says so and passes on freshness alone.
|
|
11
18
|
import { execFileSync } from 'node:child_process';
|
|
12
19
|
import { createHash } from 'node:crypto';
|
|
13
20
|
import { existsSync, readdirSync, readFileSync, statSync, realpathSync } from 'node:fs';
|
|
@@ -52,7 +59,7 @@ export async function checkSlitherReport(project, sourceDir, reportPath) {
|
|
|
52
59
|
const current = hashSources(srcAbs);
|
|
53
60
|
const data = { report: relative(project, found), sidecar: relative(project, sidecar), recordedHash: recorded, currentHash: current, sources: sources.length };
|
|
54
61
|
if (recorded === current)
|
|
55
|
-
return
|
|
62
|
+
return withFindings(project, found, `${data.report} describes the current sources: ${data.sidecar} matches the hash of ${sources.length} .sol file(s) under ${sourceDir}`, data);
|
|
56
63
|
return { id: 'slither-report', status: 'fail', reason: `${data.report} is stale: the sources under ${sourceDir} no longer match ${data.sidecar}; re-run slither.`, data };
|
|
57
64
|
}
|
|
58
65
|
const times = await fileTimes(project, [found, ...sources]);
|
|
@@ -68,9 +75,64 @@ export async function checkSlitherReport(project, sourceDir, reportPath) {
|
|
|
68
75
|
}
|
|
69
76
|
const data = { report: relative(project, found), reportTime: iso(reportTime), newestSource: relative(project, newestSource), newestSourceTime: iso(newestTime), sources: sources.length };
|
|
70
77
|
if (reportTime >= newestTime)
|
|
71
|
-
return
|
|
78
|
+
return withFindings(project, found, `${data.report} (${data.reportTime}) is newer than the newest source ${data.newestSource} (${data.newestSourceTime})`, data);
|
|
72
79
|
return { id: 'slither-report', status: 'fail', reason: `${data.report} (${data.reportTime}) is older than ${data.newestSource} (${data.newestSourceTime}); re-run slither and write ${SOURCES_HASH_FILE} beside the report (the scaffold's npm run lint:slither does both).`, data };
|
|
73
80
|
}
|
|
81
|
+
const IMPACT_RANK = { high: 3, medium: 2, low: 1, informational: 0, optimization: 0 };
|
|
82
|
+
const FAIL_ON_RANK = { high: 3, medium: 2, low: 1, informational: 0, pedantic: 0, none: 99 };
|
|
83
|
+
/** The project's `fail_on` from slither.config.json, or Slither's default, medium. */
|
|
84
|
+
export function readFailOn(project) {
|
|
85
|
+
const candidates = [join(project, 'slither.config.json'), join(project, 'contracts', 'slither.config.json')];
|
|
86
|
+
for (const file of candidates) {
|
|
87
|
+
if (!existsSync(file))
|
|
88
|
+
continue;
|
|
89
|
+
try {
|
|
90
|
+
const cfg = JSON.parse(readFileSync(file, 'utf8'));
|
|
91
|
+
if (typeof cfg.fail_on === 'string' && cfg.fail_on.toLowerCase() in FAIL_ON_RANK)
|
|
92
|
+
return cfg.fail_on.toLowerCase();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* an unreadable config falls back to the default, and the reason says which threshold applied */
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return 'medium';
|
|
99
|
+
}
|
|
100
|
+
/** Every detector result in a Slither JSON report, with one location each. */
|
|
101
|
+
export function readFindings(reportFile) {
|
|
102
|
+
if (!reportFile.endsWith('.json'))
|
|
103
|
+
return null;
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(readFileSync(reportFile, 'utf8'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const detectors = parsed.results?.detectors ?? [];
|
|
112
|
+
return detectors.map((d) => {
|
|
113
|
+
const el = d.elements?.[0]?.source_mapping;
|
|
114
|
+
const where = el?.filename_relative ? `${el.filename_relative}${el.lines?.length ? `:${el.lines[0]}` : ''}` : 'unknown location';
|
|
115
|
+
return { check: d.check ?? 'unknown', impact: d.impact ?? 'Unknown', confidence: d.confidence ?? 'Unknown', where };
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** A fresh report passes only if nothing in it reaches the project's `fail_on`. */
|
|
119
|
+
function withFindings(project, reportFile, freshness, data) {
|
|
120
|
+
const findings = readFindings(reportFile);
|
|
121
|
+
const failOn = readFailOn(project);
|
|
122
|
+
if (findings === null) {
|
|
123
|
+
return { id: 'slither-report', status: 'pass', reason: `${freshness}; its findings were not read (not a JSON report), so freshness alone is checked.`, data: { ...data, failOn, findingsRead: false } };
|
|
124
|
+
}
|
|
125
|
+
const threshold = FAIL_ON_RANK[failOn] ?? 2;
|
|
126
|
+
const blocking = findings.filter((f) => (IMPACT_RANK[f.impact.toLowerCase()] ?? 0) >= threshold);
|
|
127
|
+
const counts = findings.reduce((acc, f) => { acc[f.impact] = (acc[f.impact] ?? 0) + 1; return acc; }, {});
|
|
128
|
+
const summary = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ') || 'no findings';
|
|
129
|
+
if (blocking.length === 0) {
|
|
130
|
+
return { id: 'slither-report', status: 'pass', reason: `${freshness}; ${summary}, none at or above fail_on ${failOn}.`, data: { ...data, failOn, findingsRead: true, counts } };
|
|
131
|
+
}
|
|
132
|
+
const named = blocking.slice(0, 5).map((f) => `${f.check} (${f.impact}) at ${f.where}`).join('; ');
|
|
133
|
+
const more = blocking.length > 5 ? ` and ${blocking.length - 5} more` : '';
|
|
134
|
+
return { id: 'slither-report', status: 'fail', reason: `${data['report']} is fresh but holds ${blocking.length} finding(s) at or above fail_on ${failOn}: ${named}${more}. Fix them or triage them in the source with a reasoned slither-disable comment, then re-run slither.`, data: { ...data, failOn, findingsRead: true, counts, blocking } };
|
|
135
|
+
}
|
|
74
136
|
function iso(t) {
|
|
75
137
|
return t ? new Date(t * 1000).toISOString() : 'unknown';
|
|
76
138
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gatedpath/preflight",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Pre-flight checks before deploying to Redbelly Network (151 mainnet, 153 testnet): chain ID, deployer verification, Safe admin, compiler pins, secrets in git history, RBNT balance, Slither report.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"prepack": "npm run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@gatedpath/agent-rules": "0.2.
|
|
44
|
-
"@gatedpath/chains": "0.2.
|
|
43
|
+
"@gatedpath/agent-rules": "0.2.4",
|
|
44
|
+
"@gatedpath/chains": "0.2.4"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^22.20.2",
|