audit-notes-cli 0.1.5 → 0.1.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/dist/cli.js +83 -19
- package/dist/explain.js +33 -0
- package/dist/github/checks.js +31 -0
- package/dist/github/comment.js +43 -11
- package/dist/paid/fallbacks.js +9 -0
- package/dist/report/markdown.js +27 -3
- package/dist/report/summarize.js +37 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
+
/* ===================== INTERNAL ===================== */
|
|
9
10
|
const license_1 = require("./license");
|
|
10
11
|
const slither_1 = require("./analyzer/slither");
|
|
11
12
|
const detectors_1 = require("./analyzer/detectors");
|
|
@@ -13,9 +14,12 @@ const normalize_1 = require("./analyzer/normalize");
|
|
|
13
14
|
const compare_1 = require("./diff/compare");
|
|
14
15
|
const markdown_1 = require("./report/markdown");
|
|
15
16
|
const sarif_1 = require("./report/sarif");
|
|
17
|
+
const summarize_1 = require("./report/summarize");
|
|
16
18
|
const profiles_1 = require("./profiles/profiles");
|
|
17
19
|
const recommendations_1 = require("./paid/recommendations");
|
|
18
20
|
const fixHints_1 = require("./paid/fixHints");
|
|
21
|
+
const comment_1 = require("./github/comment");
|
|
22
|
+
const checks_1 = require("./github/checks");
|
|
19
23
|
/* ===================== VERSION ===================== */
|
|
20
24
|
const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(__dirname, "../package.json"), "utf8"));
|
|
21
25
|
const VERSION = pkg.version;
|
|
@@ -40,7 +44,48 @@ function getProfile() {
|
|
|
40
44
|
}
|
|
41
45
|
return p;
|
|
42
46
|
}
|
|
43
|
-
/* =====================
|
|
47
|
+
/* ===================== LOAD REPORT ===================== */
|
|
48
|
+
function loadReport() {
|
|
49
|
+
const p = path_1.default.resolve("output/AUDIT_NOTES.json");
|
|
50
|
+
if (!fs_1.default.existsSync(p)) {
|
|
51
|
+
throw new Error("AUDIT_NOTES.json not found. Run `audit-notes run` first.");
|
|
52
|
+
}
|
|
53
|
+
return JSON.parse(fs_1.default.readFileSync(p, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
/* ===================== EXPLAIN ===================== */
|
|
56
|
+
function explainFinding(id) {
|
|
57
|
+
const report = loadReport();
|
|
58
|
+
const finding = report.findings.find((f) => f.id === id);
|
|
59
|
+
if (!finding) {
|
|
60
|
+
throw new Error(`Finding ${id} not found`);
|
|
61
|
+
}
|
|
62
|
+
console.log(`
|
|
63
|
+
Finding ID: ${finding.id}
|
|
64
|
+
Severity: ${finding.impact}
|
|
65
|
+
Detector: ${finding.check}
|
|
66
|
+
|
|
67
|
+
Description:
|
|
68
|
+
${finding.description}
|
|
69
|
+
`);
|
|
70
|
+
// Optional PRO guidance
|
|
71
|
+
try {
|
|
72
|
+
(0, license_1.validateLicense)();
|
|
73
|
+
const rec = (0, recommendations_1.generateRecommendations)([finding])[0];
|
|
74
|
+
if (rec) {
|
|
75
|
+
console.log("\nRecommendation:");
|
|
76
|
+
console.log(rec.text);
|
|
77
|
+
}
|
|
78
|
+
const fix = (0, fixHints_1.generateFixHints)([finding])[0];
|
|
79
|
+
if (fix) {
|
|
80
|
+
console.log("\nFix Hint:");
|
|
81
|
+
console.log(fix.explanation);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* Free users – silent */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/* ===================== CORE RUN ===================== */
|
|
44
89
|
async function run() {
|
|
45
90
|
if (wantRecommendations || wantFixHints) {
|
|
46
91
|
(0, license_1.validateLicense)();
|
|
@@ -53,33 +98,53 @@ async function run() {
|
|
|
53
98
|
const raw = (0, detectors_1.analyzeDetectors)((0, slither_1.loadSlither)(slitherPath).detectors);
|
|
54
99
|
const current = (0, profiles_1.applyProfile)((0, normalize_1.normalizeDetectors)(raw), profile);
|
|
55
100
|
fs_1.default.mkdirSync("output", { recursive: true });
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
101
|
+
/* ---------- Stable machine output ---------- */
|
|
102
|
+
fs_1.default.writeFileSync("output/AUDIT_NOTES.json", JSON.stringify({
|
|
103
|
+
schemaVersion: "1.0",
|
|
104
|
+
version: VERSION,
|
|
105
|
+
findings: current
|
|
106
|
+
}, null, 2));
|
|
107
|
+
let diff = undefined;
|
|
108
|
+
/* ---------- Diff + CI gating ---------- */
|
|
61
109
|
if (useDiff) {
|
|
62
110
|
const basePath = getFlag("--base");
|
|
63
111
|
if (!basePath)
|
|
64
112
|
throw new Error("Diff requires --base");
|
|
65
113
|
const baseRaw = (0, detectors_1.analyzeDetectors)((0, slither_1.loadSlither)(basePath).detectors);
|
|
66
114
|
const base = (0, profiles_1.applyProfile)((0, normalize_1.normalizeDetectors)(baseRaw), profile);
|
|
67
|
-
|
|
115
|
+
diff = (0, compare_1.diffFindings)(base, current);
|
|
68
116
|
const blocking = diff.added.filter((f) => f.impact === "HIGH" || f.impact === "CRITICAL");
|
|
117
|
+
if (process.env.GITHUB_ACTIONS) {
|
|
118
|
+
await (0, checks_1.createCheckRun)(blocking.length ? "failure" : "success", blocking.length
|
|
119
|
+
? `❌ ${blocking.length} new HIGH/CRITICAL issues`
|
|
120
|
+
: "✅ No new HIGH/CRITICAL issues");
|
|
121
|
+
}
|
|
69
122
|
if (blocking.length)
|
|
70
123
|
process.exit(2);
|
|
71
|
-
markdown = (0, markdown_1.generateMarkdown)({ findings: current, diff });
|
|
72
124
|
}
|
|
73
|
-
|
|
74
|
-
|
|
125
|
+
/* ---------- Output formats ---------- */
|
|
126
|
+
const format = getFlag("--format") ?? "md";
|
|
127
|
+
if (format === "json")
|
|
128
|
+
return;
|
|
129
|
+
if (format === "summary") {
|
|
130
|
+
fs_1.default.writeFileSync("output/SUMMARY.md", (0, summarize_1.generateSummary)({ findings: current, diff }));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (useSarif) {
|
|
134
|
+
fs_1.default.writeFileSync("output/audit-notes.sarif", JSON.stringify((0, sarif_1.generateSarif)(current), null, 2));
|
|
135
|
+
return;
|
|
75
136
|
}
|
|
76
|
-
fs_1.default.writeFileSync("output/AUDIT_NOTES.md",
|
|
137
|
+
fs_1.default.writeFileSync("output/AUDIT_NOTES.md", (0, markdown_1.generateMarkdown)({ findings: current, diff }));
|
|
77
138
|
if (wantRecommendations) {
|
|
78
139
|
fs_1.default.writeFileSync("output/recommendations.json", JSON.stringify((0, recommendations_1.generateRecommendations)(current), null, 2));
|
|
79
140
|
}
|
|
80
141
|
if (wantFixHints) {
|
|
81
142
|
fs_1.default.writeFileSync("output/fix-hints.json", JSON.stringify((0, fixHints_1.generateFixHints)(current), null, 2));
|
|
82
143
|
}
|
|
144
|
+
if (cmd === "comment") {
|
|
145
|
+
const summary = (0, summarize_1.generateSummary)({ findings: current, diff });
|
|
146
|
+
await (0, comment_1.upsertPRComment)(summary);
|
|
147
|
+
}
|
|
83
148
|
}
|
|
84
149
|
/* ===================== ENTRY ===================== */
|
|
85
150
|
async function main() {
|
|
@@ -87,14 +152,13 @@ async function main() {
|
|
|
87
152
|
console.log(`audit-notes v${VERSION}`);
|
|
88
153
|
return;
|
|
89
154
|
}
|
|
90
|
-
if (cmd === "
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
(
|
|
95
|
-
|
|
96
|
-
else {
|
|
97
|
-
console.log(`audit-notes v${VERSION}`);
|
|
155
|
+
if (cmd === "explain") {
|
|
156
|
+
const id = args[1];
|
|
157
|
+
if (!id)
|
|
158
|
+
throw new Error("Usage: audit-notes explain <findingId>");
|
|
159
|
+
explainFinding(id);
|
|
160
|
+
return;
|
|
98
161
|
}
|
|
162
|
+
await run();
|
|
99
163
|
}
|
|
100
164
|
main();
|
package/dist/explain.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.explainFinding = explainFinding;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function explainFinding(id) {
|
|
10
|
+
const reportPath = path_1.default.resolve("output/AUDIT_NOTES.json");
|
|
11
|
+
if (!fs_1.default.existsSync(reportPath)) {
|
|
12
|
+
throw new Error("AUDIT_NOTES.json not found. Run audit-notes first.");
|
|
13
|
+
}
|
|
14
|
+
const report = JSON.parse(fs_1.default.readFileSync(reportPath, "utf8"));
|
|
15
|
+
const finding = report.findings.find((f) => f.id === id);
|
|
16
|
+
if (!finding) {
|
|
17
|
+
throw new Error(`Finding ${id} not found`);
|
|
18
|
+
}
|
|
19
|
+
console.log(`
|
|
20
|
+
Finding ID: ${finding.id}
|
|
21
|
+
Severity: ${finding.impact}
|
|
22
|
+
Detector: ${finding.detector}
|
|
23
|
+
|
|
24
|
+
Description:
|
|
25
|
+
${finding.description}
|
|
26
|
+
|
|
27
|
+
Why it matters:
|
|
28
|
+
${finding.why || "This issue may impact security, maintainability, or gas usage."}
|
|
29
|
+
|
|
30
|
+
Location:
|
|
31
|
+
${finding.file}:${finding.startLine}
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createCheckRun = createCheckRun;
|
|
7
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
+
async function createCheckRun(conclusion, summary) {
|
|
9
|
+
const token = process.env.GITHUB_TOKEN;
|
|
10
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
11
|
+
const sha = process.env.GITHUB_SHA;
|
|
12
|
+
const [owner, repoName] = repo.split("/");
|
|
13
|
+
await (0, node_fetch_1.default)(`https://api.github.com/repos/${owner}/${repoName}/check-runs`, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: {
|
|
16
|
+
Authorization: `Bearer ${token}`,
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
Accept: "application/vnd.github+json"
|
|
19
|
+
},
|
|
20
|
+
body: JSON.stringify({
|
|
21
|
+
name: "Audit Notes",
|
|
22
|
+
head_sha: sha,
|
|
23
|
+
status: "completed",
|
|
24
|
+
conclusion,
|
|
25
|
+
output: {
|
|
26
|
+
title: "Audit Notes Security Check",
|
|
27
|
+
summary
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
});
|
|
31
|
+
}
|
package/dist/github/comment.js
CHANGED
|
@@ -3,18 +3,50 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.upsertPRComment = upsertPRComment;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
8
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
const MARKER = "<!-- audit-notes -->";
|
|
10
|
+
function ghHeaders(token) {
|
|
11
|
+
return {
|
|
12
|
+
Authorization: `Bearer ${token}`,
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
Accept: "application/vnd.github+json"
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async function upsertPRComment(body) {
|
|
18
|
+
if (!process.env.GITHUB_ACTIONS) {
|
|
19
|
+
throw new Error("Not running in GitHub Actions");
|
|
20
|
+
}
|
|
21
|
+
const token = process.env.GITHUB_TOKEN;
|
|
22
|
+
if (!token)
|
|
23
|
+
throw new Error("GITHUB_TOKEN not set");
|
|
24
|
+
const event = JSON.parse(fs_1.default.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
|
25
|
+
const prNumber = event.pull_request?.number;
|
|
26
|
+
if (!prNumber)
|
|
27
|
+
throw new Error("Not a PR event");
|
|
28
|
+
const repo = process.env.GITHUB_REPOSITORY;
|
|
29
|
+
const baseUrl = `https://api.github.com/repos/${repo}/issues/${prNumber}/comments`;
|
|
30
|
+
const fullBody = `${body}\n\n${MARKER}`;
|
|
31
|
+
// 1. Fetch existing comments
|
|
32
|
+
const res = await (0, node_fetch_1.default)(baseUrl, {
|
|
33
|
+
headers: ghHeaders(token)
|
|
16
34
|
});
|
|
17
|
-
|
|
18
|
-
|
|
35
|
+
const comments = await res.json();
|
|
36
|
+
const existing = comments.find((c) => typeof c.body === "string" && c.body.includes(MARKER));
|
|
37
|
+
// 2. Update or create
|
|
38
|
+
if (existing) {
|
|
39
|
+
await (0, node_fetch_1.default)(`https://api.github.com/repos/${repo}/issues/comments/${existing.id}`, {
|
|
40
|
+
method: "PATCH",
|
|
41
|
+
headers: ghHeaders(token),
|
|
42
|
+
body: JSON.stringify({ body: fullBody })
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
await (0, node_fetch_1.default)(baseUrl, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: ghHeaders(token),
|
|
49
|
+
body: JSON.stringify({ body: fullBody })
|
|
50
|
+
});
|
|
19
51
|
}
|
|
20
52
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FALLBACK_GUIDANCE = void 0;
|
|
4
|
+
exports.FALLBACK_GUIDANCE = {
|
|
5
|
+
"unused-state-variable": "This state variable is never accessed. Consider removing it or verifying that related logic is complete.",
|
|
6
|
+
"low-level-calls": "Low-level calls bypass Solidity safety checks. Ensure return values are validated and failures handled explicitly.",
|
|
7
|
+
"reentrancy": "External calls before state updates may enable reentrancy. Review execution order carefully.",
|
|
8
|
+
"dead-code": "This code is never executed. Consider removing it to reduce attack surface and bytecode size."
|
|
9
|
+
};
|
package/dist/report/markdown.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.generateMarkdown = generateMarkdown;
|
|
|
4
4
|
function generateMarkdown(input) {
|
|
5
5
|
var _a;
|
|
6
6
|
let md = `# Smart Contract Audit Notes\n\n`;
|
|
7
|
+
/* ===================== PR SUMMARY ===================== */
|
|
7
8
|
if (input.diff) {
|
|
8
9
|
const { added, increasedRisk, unchanged } = input.diff;
|
|
9
10
|
md += `## 🔍 Pull Request Risk Summary\n\n`;
|
|
@@ -13,26 +14,27 @@ function generateMarkdown(input) {
|
|
|
13
14
|
else {
|
|
14
15
|
md += `### ❗ New Findings\n\n`;
|
|
15
16
|
for (const f of added) {
|
|
16
|
-
md += `- **${f.check}**
|
|
17
|
+
md += `- **${f.check}** (${f.impact}) — \`${f.id.slice(0, 8)}\`\n`;
|
|
17
18
|
}
|
|
18
19
|
md += `\n`;
|
|
19
20
|
}
|
|
20
21
|
if (increasedRisk.length > 0) {
|
|
21
22
|
md += `### 🔺 Risk Increased\n\n`;
|
|
22
23
|
for (const f of increasedRisk) {
|
|
23
|
-
md += `- **${f.check}**
|
|
24
|
+
md += `- **${f.check}** (${f.impact}) — \`${f.id.slice(0, 8)}\`\n`;
|
|
24
25
|
}
|
|
25
26
|
md += `\n`;
|
|
26
27
|
}
|
|
27
28
|
if (unchanged.length > 0) {
|
|
28
29
|
md += `### ⚠️ Existing Issues\n\n`;
|
|
29
30
|
for (const f of unchanged) {
|
|
30
|
-
md += `- ${f.check}
|
|
31
|
+
md += `- ${f.check} — \`${f.id.slice(0, 8)}\`\n`;
|
|
31
32
|
}
|
|
32
33
|
md += `\n`;
|
|
33
34
|
}
|
|
34
35
|
md += `---\n\n`;
|
|
35
36
|
}
|
|
37
|
+
/* ===================== FULL FINDINGS ===================== */
|
|
36
38
|
md += `## Full Findings\n\n`;
|
|
37
39
|
md += `Total findings: **${input.findings.length}**\n\n`;
|
|
38
40
|
const byImpact = {};
|
|
@@ -44,7 +46,29 @@ function generateMarkdown(input) {
|
|
|
44
46
|
md += `## ${impact} Severity\n\n`;
|
|
45
47
|
for (const f of byImpact[impact]) {
|
|
46
48
|
md += `### ${f.check}\n\n`;
|
|
49
|
+
md += `**Finding ID:** \`${f.id}\`\n\n`;
|
|
47
50
|
md += `${f.description}\n\n`;
|
|
51
|
+
/* ---------- Locations ---------- */
|
|
52
|
+
if (f.locations.length > 0) {
|
|
53
|
+
md += `**Locations:**\n\n`;
|
|
54
|
+
for (const loc of f.locations) {
|
|
55
|
+
let line = `- `;
|
|
56
|
+
if (loc.file) {
|
|
57
|
+
line += `\`${loc.file}\``;
|
|
58
|
+
}
|
|
59
|
+
if (loc.contract) {
|
|
60
|
+
line += ` • contract \`${loc.contract}\``;
|
|
61
|
+
}
|
|
62
|
+
if (loc.function) {
|
|
63
|
+
line += ` • function \`${loc.function}\``;
|
|
64
|
+
}
|
|
65
|
+
if (loc.lines && loc.lines.length > 0) {
|
|
66
|
+
line += ` • lines ${loc.lines.join(", ")}`;
|
|
67
|
+
}
|
|
68
|
+
md += `${line}\n`;
|
|
69
|
+
}
|
|
70
|
+
md += `\n`;
|
|
71
|
+
}
|
|
48
72
|
md += `---\n\n`;
|
|
49
73
|
}
|
|
50
74
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateSummary = generateSummary;
|
|
4
|
+
function generateSummary(input) {
|
|
5
|
+
const total = input.findings.length;
|
|
6
|
+
const byImpact = {
|
|
7
|
+
CRITICAL: 0,
|
|
8
|
+
HIGH: 0,
|
|
9
|
+
MEDIUM: 0,
|
|
10
|
+
LOW: 0
|
|
11
|
+
};
|
|
12
|
+
for (const f of input.findings) {
|
|
13
|
+
if (byImpact[f.impact] !== undefined) {
|
|
14
|
+
byImpact[f.impact]++;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
let md = `## 🔐 Audit Notes Summary\n\n`;
|
|
18
|
+
if (input.diff) {
|
|
19
|
+
const blockers = input.diff.added.filter(f => f.impact === "HIGH" || f.impact === "CRITICAL");
|
|
20
|
+
if (blockers.length === 0) {
|
|
21
|
+
md += `✅ No new HIGH or CRITICAL risks introduced.\n\n`;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
md += `❌ **New HIGH / CRITICAL risks introduced**\n\n`;
|
|
25
|
+
for (const f of blockers) {
|
|
26
|
+
md += `- ${f.check} — ${f.impact} (ID: \`${f.id}\`)\n`;
|
|
27
|
+
}
|
|
28
|
+
md += `\nCI blocked until resolved.\n\n`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
md += `- Total findings: ${total}\n`;
|
|
32
|
+
md += `- High / Critical: ${byImpact.HIGH + byImpact.CRITICAL}\n`;
|
|
33
|
+
md += `- Medium: ${byImpact.MEDIUM}\n`;
|
|
34
|
+
md += `- Low: ${byImpact.LOW}\n\n`;
|
|
35
|
+
md += `_See \`output/AUDIT_NOTES.md\` for full details._\n`;
|
|
36
|
+
return md;
|
|
37
|
+
}
|