Skip to content

Commit 35bd1bc

Browse files
committed
fixup! feat: add git node security --validate-reports
1 parent 330bd44 commit 35bd1bc

3 files changed

Lines changed: 81 additions & 31 deletions

File tree

components/git/security.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const securityOptions = {
6161
},
6262
'validate-reports-confirm': {
6363
default: true,
64-
describe: 'Ask before continuing to the next report after each LLM assessment',
64+
describe: 'Ask before each LLM prompt or assessment, and before continuing to the next report',
6565
type: 'boolean'
6666
},
6767
'validate-reports-cache': {
@@ -70,8 +70,8 @@ const securityOptions = {
7070
type: 'boolean'
7171
},
7272
llm: {
73-
choices: ['codex', 'claude', 'copilot'],
74-
describe: 'Ask an LLM CLI to assess each triaged report',
73+
choices: ['none', 'codex', 'claude', 'copilot'],
74+
describe: 'Print prompts for manual LLM use or ask an LLM CLI to assess each triaged report',
7575
type: 'string'
7676
},
7777
'llm-model': {

docs/git-node.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ only a local triage aid and still requires human review.
522522
git node security --validate-reports
523523
git node security --validate-reports --validate-reports-format=json
524524
git node security --validate-reports --validate-reports-output=reports.md
525+
git node security --validate-reports --llm=none
525526
git node security --validate-reports --llm=codex --node-repo=/path/to/node
526527
git node security --validate-reports --llm=codex --llm-model=gpt-5.5
527528
git node security --validate-reports --llm=codex --no-validate-reports-confirm
@@ -531,9 +532,11 @@ only a local triage aid and still requires human review.
531532
git node security --validate-reports --llm=copilot --llm-command="copilot -p"
532533
```
533534

534-
By default, the command runs a heuristic pass only. The heuristic checks the
535-
report title, vulnerability information, impact, description, comments, current
536-
severity, CVSS vector, and weakness metadata for common Node.js security topics.
535+
By default, the command runs a heuristic pass and prints the generated LLM
536+
prompt for each report so it can be copied into any LLM tool. This is the same
537+
behavior as `--llm=none`. The heuristic checks the report title, vulnerability
538+
information, impact, description, comments, current severity, CVSS vector, and
539+
weakness metadata for common Node.js security topics.
537540
It can identify obvious mismatches, such as a CVSS vector whose calculated
538541
rating does not match the HackerOne rating. Keyword matches are treated only as
539542
topic hints, not as proof that a report is valid or invalid. This is deliberate:
@@ -543,11 +546,12 @@ heuristic output is deliberately conservative and always leaves threat-model
543546
validity as `needs-manual-review`.
544547

545548
Use `--llm=<provider>` to ask an LLM CLI to produce a structured assessment for
546-
each report. Supported providers are `codex`, `claude`, and `copilot`.
549+
each report. Supported providers are `codex`, `claude`, and `copilot`. Use
550+
`--llm=none`, or omit `--llm`, to print the prompt without running an LLM CLI.
547551

548-
When LLM mode is enabled, the command asks before assessing each report and shows
549-
the report title, current severity, CVSS vector, and weakness. After each LLM
550-
assessment, it prints a readable summary with:
552+
When an LLM CLI provider is enabled, the command asks before assessing each
553+
report and shows the report title, current severity, CVSS vector, and weakness.
554+
After each LLM assessment, it prints a readable summary with:
551555

552556
- the report URL and title
553557
- the provider and model/cache identity
@@ -560,9 +564,11 @@ assessment, it prints a readable summary with:
560564
- threat model/documentation references used by the model
561565
- reasoning
562566

563-
Use `--no-validate-reports-confirm` for batch mode without the per-report
564-
prompts. Use `--validate-reports-limit=<n>` to test the flow against a smaller
565-
number of reports.
567+
In manual prompt mode, the command asks before printing each report prompt and
568+
then asks whether to continue to the next report. Use
569+
`--no-validate-reports-confirm` for batch mode without the per-report prompts.
570+
Use `--validate-reports-limit=<n>` to test the flow against a smaller number of
571+
reports.
566572

567573
#### LLM prompt and reasoning
568574

@@ -627,11 +633,11 @@ assessment.
627633
| `--validate-reports-format=markdown\|json` | Select the final output format. Defaults to `markdown`. |
628634
| `--validate-reports-output=<file>` | Write the final output to a file instead of stdout. |
629635
| `--validate-reports-limit=<n>` | Validate at most `n` triaged reports. Useful for testing the flow. |
630-
| `--validate-reports-confirm` | Ask before each LLM assessment and before continuing to the next report. Enabled by default. |
636+
| `--validate-reports-confirm` | Ask before each LLM prompt or assessment, and before continuing to the next report. Enabled by default. |
631637
| `--no-validate-reports-confirm` | Disable interactive prompts for batch runs. |
632638
| `--validate-reports-cache` | Reuse cached successful LLM assessments. Enabled by default. |
633639
| `--no-validate-reports-cache` | Ignore existing LLM cache entries and do not reuse them. |
634-
| `--llm=codex\|claude\|copilot` | Ask an LLM CLI to assess each report. |
640+
| `--llm=none\|codex\|claude\|copilot` | Print prompts for manual LLM use or ask an LLM CLI to assess each report. Defaults to `none`. |
635641
| `--llm-model=<model>` | Override the provider model and cache identity. |
636642
| `--llm-command=<command>` | Override the command used for LLM assessment. The prompt is sent on stdin. |
637643
| `--node-repo=<path>` | Path to a Node.js checkout containing `SECURITY.md` and `doc/`. Defaults to the current directory. |

lib/validate_reports.js

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const H1_TRIAGED_REPORTS_URL =
1616
'https://api.hackerone.com/v1/reports?filter[program][]=nodejs&filter[state][]=triaged';
1717
const CACHE_FOLDER = '.ncu-cache/security-report-validation';
1818
const MANUAL_REVIEW_VALIDITY = 'needs-manual-review';
19+
const MANUAL_LLM_PROVIDER = 'none';
1920

2021
const CVSS_WEIGHTS = {
2122
AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 },
@@ -519,14 +520,19 @@ function reportToMarkdown(result) {
519520
` - References: ${
520521
result.llm.assessment.threat_model_references.join(', ')}`
521522
].join('\n')
522-
: result.llm?.error
523+
: result.llm?.promptPrinted
523524
? [
524525
` - Provider: ${result.llm.provider}`,
525-
` - Error: ${result.llm.error}`
526+
' - Prompt printed to terminal for manual LLM assessment'
526527
].join('\n')
527-
: result.llm?.skipped
528-
? ` - Provider: ${result.llm.provider}\n - Skipped by user`
529-
: ' - Not requested';
528+
: result.llm?.error
529+
? [
530+
` - Provider: ${result.llm.provider}`,
531+
` - Error: ${result.llm.error}`
532+
].join('\n')
533+
: result.llm?.skipped
534+
? ` - Provider: ${result.llm.provider}\n - Skipped by user`
535+
: ' - Not requested';
530536

531537
return [
532538
`### ${result.id}: ${result.title}`,
@@ -777,6 +783,10 @@ function inferLLMModel(provider, explicitModel) {
777783
}
778784

779785
function buildProviderCommand(provider, nodeRepo, commandOverride, model) {
786+
if (provider === MANUAL_LLM_PROVIDER) {
787+
return;
788+
}
789+
780790
if (commandOverride) {
781791
return {
782792
command: commandOverride,
@@ -845,6 +855,18 @@ function buildProviderCommand(provider, nodeRepo, commandOverride, model) {
845855
}
846856
}
847857

858+
function printLLMPrompt(result, prompt, cli) {
859+
cli.separator(`H1 ${result.id} LLM Prompt`);
860+
cli.log([
861+
'Copy this prompt into any LLM tool. The response should be JSON matching ' +
862+
'the schema included in the prompt.',
863+
'',
864+
'----- BEGIN LLM PROMPT -----',
865+
prompt.trimEnd(),
866+
'----- END LLM PROMPT -----'
867+
].join('\n'));
868+
}
869+
848870
function cacheDir() {
849871
return path.join(process.cwd(), CACHE_FOLDER);
850872
}
@@ -879,11 +901,11 @@ function writeCachedAssessment(key, assessment, metadata = {}) {
879901
fs.writeFileSync(file, JSON.stringify(cache, null, 2) + '\n');
880902
}
881903

882-
// This is the actual prompt sent to Codex, Claude, Copilot, or --llm-command.
883-
// The command receives it on stdin and must return JSON matching
884-
// LLM_OUTPUT_SCHEMA. Keep this prompt explicit about SECURITY.md and doc/
885-
// because the model should make a threat-model decision from Node.js sources,
886-
// not only from reporter-controlled HackerOne text.
904+
// This is the actual prompt printed for manual LLM use or sent to Codex,
905+
// Claude, Copilot, or --llm-command. Commands receive it on stdin and must
906+
// return JSON matching LLM_OUTPUT_SCHEMA. Keep this prompt explicit about
907+
// SECURITY.md and doc/ because the model should make a threat-model decision
908+
// from Node.js sources, not only from reporter-controlled HackerOne text.
887909
function buildLLMPrompt(report, heuristic, nodeRepo, allReports) {
888910
const payload = getReportPromptPayload(report, heuristic, allReports);
889911
return `You are assessing a private HackerOne report for Node.js core.
@@ -987,6 +1009,16 @@ async function assessOneReportWithLLM({
9871009
}
9881010

9891011
const prompt = buildLLMPrompt(report, result, nodeRepo, reports);
1012+
1013+
if (provider === MANUAL_LLM_PROVIDER) {
1014+
printLLMPrompt(result, prompt, cli);
1015+
result.llm = {
1016+
provider,
1017+
promptPrinted: true
1018+
};
1019+
return promptAfterManualLLMPrompt(argv, cli);
1020+
}
1021+
9901022
const key = cacheKey({
9911023
provider,
9921024
model,
@@ -1056,7 +1088,7 @@ async function assessOneReportWithLLM({
10561088

10571089
async function assessReportsWithLLM(reports, results, argv, cli) {
10581090
const nodeRepo = path.resolve(argv['node-repo'] ?? process.cwd());
1059-
const provider = argv.llm;
1091+
const provider = argv.llm ?? MANUAL_LLM_PROVIDER;
10601092
const explicitModel = argv['llm-model'];
10611093
const modelInfo = inferLLMModel(provider, explicitModel);
10621094
const model = modelInfo.model;
@@ -1086,7 +1118,7 @@ async function assessReportsWithLLM(reports, results, argv, cli) {
10861118
if (!shouldContinue) break;
10871119
}
10881120
} finally {
1089-
commandConfig.cleanup?.();
1121+
commandConfig?.cleanup?.();
10901122
}
10911123
}
10921124

@@ -1105,7 +1137,11 @@ async function promptBeforeLLMAssessment(result, argv, cli, index, total) {
11051137
`${result.weakness.name || ''}`.trim()
11061138
].join('\n'));
11071139

1108-
return cli.prompt(`Assess H1 report ${result.id}: ${result.title}?`, {
1140+
const action = (argv.llm ?? MANUAL_LLM_PROVIDER) === MANUAL_LLM_PROVIDER
1141+
? 'Print LLM prompt for'
1142+
: 'Assess';
1143+
1144+
return cli.prompt(`${action} H1 report ${result.id}: ${result.title}?`, {
11091145
defaultAnswer: true
11101146
});
11111147
}
@@ -1123,6 +1159,16 @@ async function promptAfterLLMAssessment(result, argv, cli) {
11231159
});
11241160
}
11251161

1162+
async function promptAfterManualLLMPrompt(argv, cli) {
1163+
if (!argv['validate-reports-confirm']) {
1164+
return true;
1165+
}
1166+
1167+
return cli.prompt('Continue to the next report?', {
1168+
defaultAnswer: true
1169+
});
1170+
}
1171+
11261172
export default class ValidateReports {
11271173
constructor(cli, argv = {}) {
11281174
this.cli = cli;
@@ -1144,9 +1190,7 @@ export default class ValidateReports {
11441190
this.cli.stopSpinner(`Fetched ${reports.length} triaged HackerOne reports`);
11451191

11461192
const results = reports.map(assessReport);
1147-
if (this.argv.llm) {
1148-
await assessReportsWithLLM(reports, results, this.argv, this.cli);
1149-
}
1193+
await assessReportsWithLLM(reports, results, this.argv, this.cli);
11501194

11511195
const format = this.argv['validate-reports-format'] ?? 'markdown';
11521196
const output = format === 'json'

0 commit comments

Comments
 (0)