Skip to content

Commit a396013

Browse files
author
Ramachandran R
committed
Refactor scan-site and security-review skills for improved user experience and code clarity
- Updated SKILL.md for scan-site to streamline user options and remove unnecessary confirmation steps before scanning. - Enhanced get-latest-report.js to improve error handling and modularize the main function. - Refactored poll-deep-scan.js to introduce better timeout handling and improve readability. - Simplified start-deep-scan.js by consolidating argument parsing and error handling. - Transformed transform-report.js to improve report parsing and error messaging, ensuring better handling of malformed reports. - Revised security-review SKILL.md to reduce phases from seven to six, clarifying workflow and user interactions. - Updated flow.md to reflect changes in the security-review skill phases and improve clarity on user-facing beats.
1 parent 1686d49 commit a396013

20 files changed

Lines changed: 1070 additions & 722 deletions

File tree

Lines changed: 143 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
#!/usr/bin/env node
2-
// Consolidates per-skill review JSON files into the data file consumed by render-review.js.
3-
// Run with --help for flags.
42

53
const fs = require('fs');
64
const path = require('path');
75

8-
if (process.argv.includes('--help')) {
9-
process.stdout.write(`build-review-data.js — Consolidate per-skill JSON into a single review data file.
6+
const REQUIRED_FLAGS = [
7+
'reportName',
8+
'inputDir',
9+
'siteName',
10+
'goalLabel',
11+
'scopeLabel',
12+
'output',
13+
];
14+
15+
const SECTION_MAP = {
16+
'scan-site.json': { id: 'site-scan', label: 'Live Site Scan', icon: '◐' },
17+
'manage-headers.json': { id: 'headers', label: 'Browser Headers', icon: '◑' },
18+
'manage-firewall.json': {
19+
id: 'firewall',
20+
label: 'Web Application Firewall',
21+
icon: '◆',
22+
},
23+
'audit-permissions.json': { id: 'permissions', label: 'Roles & Permissions', icon: '◇' },
24+
'setup-auth.json': { id: 'auth', label: 'Access & Identity', icon: '◈' },
25+
};
26+
27+
// Every severity a finding may carry. `pass` is shown as a stat but excluded
28+
// from the issue count by the report template.
29+
const SEVERITIES = ['critical', 'high', 'warning', 'medium', 'info', 'low', 'pass'];
30+
31+
const HELP = `build-review-data.js — Consolidate per-skill JSON into a single review data file.
1032
1133
Usage:
1234
node build-review-data.js --reportName <name> --inputDir <dir> --siteName <name> --goalLabel <label> --scopeLabel <label> --output <path> [--summary <text>] [--nextStepsFile <path>]
@@ -25,143 +47,148 @@ Flags:
2547
Exit codes:
2648
0 Success (data file written; status JSON on stdout)
2749
1 Invocation error (missing flag or unreadable input dir)
28-
29-
Examples:
30-
node build-review-data.js --reportName "<report-name>" --inputDir <input-dir> --siteName "<site-name>" --goalLabel "<goal-label>" --scopeLabel "<scope-label>" --output <output-file>
31-
node build-review-data.js --reportName "<report-name>" --inputDir <input-dir> --siteName "<site-name>" --goalLabel "<goal-label>" --scopeLabel "<scope-label>" --summary "<summary-text>" --nextStepsFile <next-steps-file> --output <output-file>
32-
`);
33-
process.exit(0);
34-
}
50+
`;
3551

3652
function getArg(name, fallback = null) {
3753
const idx = process.argv.indexOf('--' + name);
3854
return idx !== -1 && idx + 1 < process.argv.length ? process.argv[idx + 1] : fallback;
3955
}
4056

41-
const reportName = getArg('reportName');
42-
const inputDir = getArg('inputDir');
43-
const siteName = getArg('siteName');
44-
const goalLabel = getArg('goalLabel');
45-
const scopeLabel = getArg('scopeLabel');
46-
const outputPath = getArg('output');
47-
const summaryArg = getArg('summary', '');
48-
const nextStepsFile = getArg('nextStepsFile');
49-
50-
for (const [name, value] of [['reportName', reportName], ['inputDir', inputDir], ['siteName', siteName], ['goalLabel', goalLabel], ['scopeLabel', scopeLabel], ['output', outputPath]]) {
51-
if (!value) {
52-
process.stderr.write(`Missing required flag: --${name}\n`);
53-
process.exit(1);
57+
function readNextSteps(filePath) {
58+
try {
59+
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
60+
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === 'string') : [];
61+
} catch (err) {
62+
process.stderr.write(`Could not read next-steps file: ${err.message}\n`);
63+
return [];
5464
}
5565
}
5666

57-
if (!fs.existsSync(inputDir)) {
58-
process.stderr.write(`Input dir not found: ${inputDir}\n`);
59-
process.exit(1);
67+
function formatGeneratedAt(now) {
68+
const pad = (n) => String(n).padStart(2, '0');
69+
// Intl returns "GMT+5:30" on some platforms; keep only the short abbreviation when present.
70+
const tzName =
71+
new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' })
72+
.formatToParts(now)
73+
.find((p) => p.type === 'timeZoneName')?.value || '';
74+
const stamp =
75+
`${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
76+
`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
77+
return tzName ? `${stamp} ${tzName}` : stamp;
6078
}
6179

62-
const SECTION_MAP = {
63-
'scan-site.json': { id: 'site-scan', label: 'Live Site Scan', icon: '◐' },
64-
'manage-headers.json': { id: 'headers', label: 'Browser Headers', icon: '◑' },
65-
'manage-firewall.json': { id: 'firewall', label: 'Web Application Firewall', icon: '◆' },
66-
'audit-permissions.json': { id: 'permissions', label: 'Roles & Permissions', icon: '◇' },
67-
'setup-auth.json': { id: 'auth', label: 'Access & Identity', icon: '◈' },
68-
};
69-
70-
// Severities that may appear on findings — ordered by precedence (most severe first).
71-
// pass is excluded from the "issue" count but still shown as its own stat.
72-
const SEVERITIES = ['critical', 'high', 'warning', 'medium', 'info', 'low', 'pass'];
73-
74-
const sections = [];
75-
const totals = Object.fromEntries(SEVERITIES.map(s => [s, 0]));
76-
77-
for (const fileName of fs.readdirSync(inputDir).sort()) {
78-
if (!fileName.endsWith('.json')) continue;
79-
if (fileName === path.basename(outputPath)) continue;
80-
const filePath = path.join(inputDir, fileName);
81-
82-
let raw;
83-
try {
84-
raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
85-
} catch (err) {
86-
process.stderr.write(`Skipping ${fileName}: ${err.message}\n`);
87-
continue;
88-
}
89-
90-
// Skip files that aren't recognized sections (intermediate tool outputs, helper files like next-steps).
91-
if (!SECTION_MAP[fileName]) continue;
92-
const meta = SECTION_MAP[fileName];
80+
function skippedSection(meta, reason) {
81+
return {
82+
id: meta.id,
83+
icon: meta.icon,
84+
label: meta.label,
85+
description: '',
86+
findings: [
87+
{
88+
id: `${meta.id}-skipped`,
89+
severity: 'info',
90+
title: `${meta.label} check was skipped`,
91+
details: reason || 'No additional detail.',
92+
},
93+
],
94+
details: {},
95+
};
96+
}
9397

94-
if (raw && raw.status === 'skipped') {
98+
function buildSections(inputDir, outputBasename) {
99+
const sections = [];
100+
const totals = Object.fromEntries(SEVERITIES.map((s) => [s, 0]));
101+
102+
for (const fileName of fs.readdirSync(inputDir).sort()) {
103+
if (!fileName.endsWith('.json')) continue;
104+
if (fileName === outputBasename) continue;
105+
const meta = SECTION_MAP[fileName];
106+
if (!meta) continue;
107+
108+
const filePath = path.join(inputDir, fileName);
109+
let raw;
110+
try {
111+
raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
112+
} catch (err) {
113+
process.stderr.write(`Skipping ${fileName}: ${err.message}\n`);
114+
continue;
115+
}
116+
117+
if (raw?.status === 'skipped') {
118+
sections.push(skippedSection(meta, raw.reason));
119+
totals.info += 1;
120+
continue;
121+
}
122+
123+
const findings = Array.isArray(raw?.findings) ? raw.findings : [];
95124
sections.push({
96125
id: meta.id,
97126
icon: meta.icon,
98127
label: meta.label,
99128
description: '',
100-
findings: [{
101-
id: `${meta.id}-skipped`,
102-
severity: 'info',
103-
title: `${meta.label} check was skipped`,
104-
details: raw.reason || 'No additional detail.',
105-
}],
106-
details: {},
129+
findings,
130+
details: raw?.details || {},
107131
});
108-
totals.info += 1;
109-
continue;
132+
133+
for (const f of findings) {
134+
if (f.severity && totals[f.severity] !== undefined) totals[f.severity] += 1;
135+
}
110136
}
111137

112-
const findings = Array.isArray(raw?.findings) ? raw.findings : [];
113-
const details = raw?.details || {};
138+
return { sections, totals };
139+
}
114140

115-
sections.push({
116-
id: meta.id,
117-
icon: meta.icon,
118-
label: meta.label,
119-
description: '',
120-
findings,
121-
details,
122-
});
141+
function main() {
142+
if (process.argv.includes('--help')) {
143+
process.stdout.write(HELP);
144+
return;
145+
}
123146

124-
for (const f of findings) {
125-
if (f.severity && totals[f.severity] !== undefined) totals[f.severity] += 1;
147+
const values = Object.fromEntries(REQUIRED_FLAGS.map((flag) => [flag, getArg(flag)]));
148+
for (const flag of REQUIRED_FLAGS) {
149+
if (!values[flag]) {
150+
process.stderr.write(`Missing required flag: --${flag}\n`);
151+
process.exit(1);
152+
}
126153
}
127-
}
128154

129-
let nextSteps = [];
130-
if (nextStepsFile) {
131-
try {
132-
const ns = JSON.parse(fs.readFileSync(nextStepsFile, 'utf8'));
133-
if (Array.isArray(ns)) nextSteps = ns.filter(x => typeof x === 'string');
134-
} catch (err) {
135-
process.stderr.write(`Could not read next-steps file: ${err.message}\n`);
155+
const inputDir = values.inputDir;
156+
const outputPath = values.output;
157+
if (!fs.existsSync(inputDir)) {
158+
process.stderr.write(`Input dir not found: ${inputDir}\n`);
159+
process.exit(1);
136160
}
137-
}
138161

139-
const now = new Date();
140-
const pad = (n) => String(n).padStart(2, '0');
141-
// Local-timezone short name (e.g. "IST", "PST"). Intl gives "GMT+5:30" on some platforms — strip to just the abbreviation when present.
142-
const tzName = new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }).formatToParts(now).find(p => p.type === 'timeZoneName')?.value || '';
143-
const generatedAt = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}${tzName ? ' ' + tzName : ''}`;
144-
145-
const payload = {
146-
REPORT_NAME: reportName,
147-
SITE_NAME: siteName,
148-
GOAL_LABEL: goalLabel,
149-
SCOPE_LABEL: scopeLabel,
150-
GENERATED_AT: generatedAt,
151-
REVIEW_DATA: {
152-
summary: summaryArg || '',
153-
totals,
154-
sections,
155-
nextSteps,
156-
},
157-
};
162+
const summary = getArg('summary', '');
163+
const nextStepsFile = getArg('nextStepsFile');
164+
const nextSteps = nextStepsFile ? readNextSteps(nextStepsFile) : [];
165+
166+
const { sections, totals } = buildSections(inputDir, path.basename(outputPath));
167+
168+
const payload = {
169+
REPORT_NAME: values.reportName,
170+
SITE_NAME: values.siteName,
171+
GOAL_LABEL: values.goalLabel,
172+
SCOPE_LABEL: values.scopeLabel,
173+
GENERATED_AT: formatGeneratedAt(new Date()),
174+
REVIEW_DATA: { summary: summary || '', totals, sections, nextSteps },
175+
};
176+
177+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
178+
fs.writeFileSync(outputPath, JSON.stringify(payload, null, 2));
179+
180+
process.stdout.write(
181+
JSON.stringify({
182+
status: 'ok',
183+
outputPath,
184+
totals,
185+
sectionsCount: sections.length,
186+
}) + '\n'
187+
);
188+
}
158189

159-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
160-
fs.writeFileSync(outputPath, JSON.stringify(payload, null, 2));
190+
if (require.main === module) {
191+
main();
192+
}
161193

162-
process.stdout.write(JSON.stringify({
163-
status: 'ok',
164-
outputPath,
165-
totals,
166-
sectionsCount: sections.length,
167-
}) + '\n');
194+
module.exports = { buildSections, formatGeneratedAt, SECTION_MAP, SEVERITIES };

0 commit comments

Comments
 (0)