@vigolium/piolium 0.0.1
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/LICENSE +21 -0
- package/README.md +117 -0
- package/agents/access-auditor.md +300 -0
- package/agents/assumption-breaker.md +154 -0
- package/agents/attack-designer.md +116 -0
- package/agents/code-scanner.md +139 -0
- package/agents/concurrency-auditor.md +238 -0
- package/agents/confirm-writer.md +257 -0
- package/agents/context-reviewer.md +274 -0
- package/agents/cross-verifier.md +165 -0
- package/agents/cve-scout.md +381 -0
- package/agents/env-builder.md +282 -0
- package/agents/env-profiler.md +205 -0
- package/agents/evidence-collector.md +140 -0
- package/agents/finding-grader.md +142 -0
- package/agents/finding-writer.md +148 -0
- package/agents/flow-tracer.md +106 -0
- package/agents/goal-backtracer.md +146 -0
- package/agents/history-miner.md +467 -0
- package/agents/independent-verifier.md +118 -0
- package/agents/intent-mapper.md +183 -0
- package/agents/longshot-collector.md +128 -0
- package/agents/longshot-prober.md +126 -0
- package/agents/patch-auditor.md +73 -0
- package/agents/poc-author.md +124 -0
- package/agents/poc-runner.md +194 -0
- package/agents/probe-lead.md +269 -0
- package/agents/red-challenger.md +101 -0
- package/agents/report-composer.md +208 -0
- package/agents/review-adjudicator.md +216 -0
- package/agents/spec-auditor.md +155 -0
- package/agents/taint-tracer.md +265 -0
- package/agents/test-locator.md +209 -0
- package/agents/threat-modeler.md +132 -0
- package/agents/variant-scanner.md +108 -0
- package/agents/variant-spotter.md +110 -0
- package/bin/piolium.mjs +376 -0
- package/extensions/piolium/_vendor/yaml.bundle.d.mts +6 -0
- package/extensions/piolium/_vendor/yaml.bundle.mjs +139 -0
- package/extensions/piolium/agent-runner.ts +322 -0
- package/extensions/piolium/agents.ts +266 -0
- package/extensions/piolium/audit-state.ts +522 -0
- package/extensions/piolium/bundled-resources.ts +97 -0
- package/extensions/piolium/candidate-scan.ts +966 -0
- package/extensions/piolium/command-target.ts +177 -0
- package/extensions/piolium/console-stream.ts +57 -0
- package/extensions/piolium/export-results.ts +380 -0
- package/extensions/piolium/findings.ts +448 -0
- package/extensions/piolium/heartbeat.ts +182 -0
- package/extensions/piolium/help.ts +234 -0
- package/extensions/piolium/index.ts +1865 -0
- package/extensions/piolium/longshot.ts +530 -0
- package/extensions/piolium/matcher-suggestions.ts +196 -0
- package/extensions/piolium/matcher-utils.ts +83 -0
- package/extensions/piolium/modes/balanced.ts +750 -0
- package/extensions/piolium/modes/confirm-bootstrap.ts +186 -0
- package/extensions/piolium/modes/confirm.ts +697 -0
- package/extensions/piolium/modes/deep.ts +917 -0
- package/extensions/piolium/modes/diff.ts +177 -0
- package/extensions/piolium/modes/lite.ts +540 -0
- package/extensions/piolium/modes/longshot.ts +595 -0
- package/extensions/piolium/modes/merge.ts +204 -0
- package/extensions/piolium/modes/phase-runner.ts +267 -0
- package/extensions/piolium/modes/reinvest.ts +546 -0
- package/extensions/piolium/modes/revisit.ts +279 -0
- package/extensions/piolium/modes.ts +48 -0
- package/extensions/piolium/phase-labels.ts +123 -0
- package/extensions/piolium/phase-status-strip.ts +92 -0
- package/extensions/piolium/prompt-prefix-editor.ts +39 -0
- package/extensions/piolium/providers/anthropic-vertex.ts +836 -0
- package/extensions/piolium/recon.ts +409 -0
- package/extensions/piolium/result-stats.ts +105 -0
- package/extensions/piolium/retry.ts +120 -0
- package/extensions/piolium/scheduler.ts +212 -0
- package/extensions/piolium/secrets.ts +368 -0
- package/extensions/piolium/tools/web-tools.ts +148 -0
- package/package.json +77 -0
- package/skills/agentic-actions-auditor/SKILL.md +327 -0
- package/skills/agentic-actions-auditor/references/action-profiles.md +186 -0
- package/skills/agentic-actions-auditor/references/cross-file-resolution.md +209 -0
- package/skills/agentic-actions-auditor/references/foundations.md +94 -0
- package/skills/agentic-actions-auditor/references/vector-a-env-var-intermediary.md +77 -0
- package/skills/agentic-actions-auditor/references/vector-b-direct-expression-injection.md +83 -0
- package/skills/agentic-actions-auditor/references/vector-c-cli-data-fetch.md +83 -0
- package/skills/agentic-actions-auditor/references/vector-d-pr-target-checkout.md +88 -0
- package/skills/agentic-actions-auditor/references/vector-e-error-log-injection.md +88 -0
- package/skills/agentic-actions-auditor/references/vector-f-subshell-expansion.md +82 -0
- package/skills/agentic-actions-auditor/references/vector-g-eval-of-ai-output.md +91 -0
- package/skills/agentic-actions-auditor/references/vector-h-dangerous-sandbox-configs.md +102 -0
- package/skills/agentic-actions-auditor/references/vector-i-wildcard-allowlists.md +88 -0
- package/skills/audit/SKILL.md +562 -0
- package/skills/audit/assets/icon.svg +7 -0
- package/skills/audit/hooks/scripts/validate_phase_output.py +550 -0
- package/skills/audit/references/adversarial-review.md +148 -0
- package/skills/audit/references/architecture-aware-sast.md +306 -0
- package/skills/audit/references/audit-workflow.md +737 -0
- package/skills/audit/references/chamber-protocol.md +384 -0
- package/skills/audit/references/creative-attack-modes.md +221 -0
- package/skills/audit/references/deep-analysis.md +273 -0
- package/skills/audit/references/domain-attack-playbooks.md +1129 -0
- package/skills/audit/references/knowledge-base-template.md +513 -0
- package/skills/audit/references/real-env-validation.md +191 -0
- package/skills/audit/references/report-templates.md +417 -0
- package/skills/audit/references/triage-and-prereqs.md +134 -0
- package/skills/audit/scripts/consolidate_drafts.py +554 -0
- package/skills/audit/scripts/partition_findings.py +152 -0
- package/skills/audit/scripts/rg-hotspots.sh +121 -0
- package/skills/audit/scripts/stamp_file_state.py +349 -0
- package/skills/code-reviewer/SKILL.md +65 -0
- package/skills/codeql/SKILL.md +281 -0
- package/skills/codeql/references/build-fixes.md +90 -0
- package/skills/codeql/references/diagnostic-query-templates.md +339 -0
- package/skills/codeql/references/extension-yaml-format.md +209 -0
- package/skills/codeql/references/important-only-suite.md +153 -0
- package/skills/codeql/references/language-details.md +207 -0
- package/skills/codeql/references/macos-arm64e-workaround.md +179 -0
- package/skills/codeql/references/performance-tuning.md +111 -0
- package/skills/codeql/references/quality-assessment.md +172 -0
- package/skills/codeql/references/ruleset-catalog.md +63 -0
- package/skills/codeql/references/run-all-suite.md +92 -0
- package/skills/codeql/references/sarif-processing.md +79 -0
- package/skills/codeql/references/threat-models.md +51 -0
- package/skills/codeql/workflows/build-database.md +280 -0
- package/skills/codeql/workflows/create-data-extensions.md +261 -0
- package/skills/codeql/workflows/run-analysis.md +301 -0
- package/skills/differential-review/SKILL.md +220 -0
- package/skills/differential-review/adversarial.md +203 -0
- package/skills/differential-review/methodology.md +234 -0
- package/skills/differential-review/patterns.md +300 -0
- package/skills/differential-review/reporting.md +369 -0
- package/skills/fp-check/SKILL.md +125 -0
- package/skills/fp-check/references/bug-class-verification.md +114 -0
- package/skills/fp-check/references/deep-verification.md +143 -0
- package/skills/fp-check/references/evidence-templates.md +91 -0
- package/skills/fp-check/references/false-positive-patterns.md +115 -0
- package/skills/fp-check/references/gate-reviews.md +27 -0
- package/skills/fp-check/references/standard-verification.md +78 -0
- package/skills/insecure-defaults/SKILL.md +117 -0
- package/skills/insecure-defaults/references/examples.md +409 -0
- package/skills/last30days/SKILL.md +444 -0
- package/skills/sarif-parsing/SKILL.md +483 -0
- package/skills/sarif-parsing/resources/jq-queries.md +162 -0
- package/skills/sarif-parsing/resources/sarif_helpers.py +331 -0
- package/skills/security-threat-model/LICENSE.txt +201 -0
- package/skills/security-threat-model/SKILL.md +81 -0
- package/skills/security-threat-model/agents/openai.yaml +4 -0
- package/skills/security-threat-model/references/prompt-template.md +255 -0
- package/skills/security-threat-model/references/security-controls-and-assets.md +32 -0
- package/skills/semgrep/SKILL.md +212 -0
- package/skills/semgrep/references/rulesets.md +162 -0
- package/skills/semgrep/references/scan-modes.md +110 -0
- package/skills/semgrep/references/scanner-task-prompt.md +140 -0
- package/skills/semgrep/scripts/merge_sarif.py +203 -0
- package/skills/semgrep/workflows/scan-workflow.md +311 -0
- package/skills/semgrep-rule-creator/SKILL.md +168 -0
- package/skills/semgrep-rule-creator/references/quick-reference.md +202 -0
- package/skills/semgrep-rule-creator/references/workflow.md +240 -0
- package/skills/semgrep-rule-variant-creator/SKILL.md +205 -0
- package/skills/semgrep-rule-variant-creator/references/applicability-analysis.md +250 -0
- package/skills/semgrep-rule-variant-creator/references/language-syntax-guide.md +324 -0
- package/skills/semgrep-rule-variant-creator/references/workflow.md +518 -0
- package/skills/sharp-edges/SKILL.md +292 -0
- package/skills/sharp-edges/references/auth-patterns.md +252 -0
- package/skills/sharp-edges/references/case-studies.md +274 -0
- package/skills/sharp-edges/references/config-patterns.md +333 -0
- package/skills/sharp-edges/references/crypto-apis.md +190 -0
- package/skills/sharp-edges/references/lang-c.md +205 -0
- package/skills/sharp-edges/references/lang-csharp.md +285 -0
- package/skills/sharp-edges/references/lang-go.md +270 -0
- package/skills/sharp-edges/references/lang-java.md +263 -0
- package/skills/sharp-edges/references/lang-javascript.md +269 -0
- package/skills/sharp-edges/references/lang-kotlin.md +265 -0
- package/skills/sharp-edges/references/lang-php.md +245 -0
- package/skills/sharp-edges/references/lang-python.md +274 -0
- package/skills/sharp-edges/references/lang-ruby.md +273 -0
- package/skills/sharp-edges/references/lang-rust.md +272 -0
- package/skills/sharp-edges/references/lang-swift.md +287 -0
- package/skills/sharp-edges/references/language-specific.md +588 -0
- package/skills/spec-to-code-compliance/SKILL.md +357 -0
- package/skills/spec-to-code-compliance/resources/COMPLETENESS_CHECKLIST.md +69 -0
- package/skills/spec-to-code-compliance/resources/IR_EXAMPLES.md +417 -0
- package/skills/spec-to-code-compliance/resources/OUTPUT_REQUIREMENTS.md +105 -0
- package/skills/supply-chain-risk-auditor/SKILL.md +67 -0
- package/skills/supply-chain-risk-auditor/resources/results-template.md +41 -0
- package/skills/variant-analysis/METHODOLOGY.md +327 -0
- package/skills/variant-analysis/SKILL.md +142 -0
- package/skills/variant-analysis/resources/codeql/cpp.ql +119 -0
- package/skills/variant-analysis/resources/codeql/go.ql +69 -0
- package/skills/variant-analysis/resources/codeql/java.ql +71 -0
- package/skills/variant-analysis/resources/codeql/javascript.ql +63 -0
- package/skills/variant-analysis/resources/codeql/python.ql +80 -0
- package/skills/variant-analysis/resources/semgrep/cpp.yaml +98 -0
- package/skills/variant-analysis/resources/semgrep/go.yaml +63 -0
- package/skills/variant-analysis/resources/semgrep/java.yaml +61 -0
- package/skills/variant-analysis/resources/semgrep/javascript.yaml +60 -0
- package/skills/variant-analysis/resources/semgrep/python.yaml +72 -0
- package/skills/variant-analysis/resources/variant-report-template.md +75 -0
- package/skills/vuln-report/SKILL.md +137 -0
- package/skills/vuln-report/agents/openai.yaml +4 -0
- package/skills/vuln-report/references/report-template.md +135 -0
- package/skills/wooyun-legacy/SKILL.md +367 -0
- package/skills/wooyun-legacy/references/bank-penetration.md +222 -0
- package/skills/wooyun-legacy/references/checklists/command-execution-checklist.md +119 -0
- package/skills/wooyun-legacy/references/checklists/csrf-checklist.md +74 -0
- package/skills/wooyun-legacy/references/checklists/file-upload-checklist.md +108 -0
- package/skills/wooyun-legacy/references/checklists/info-disclosure-checklist.md +114 -0
- package/skills/wooyun-legacy/references/checklists/logic-flaws-checklist.md +95 -0
- package/skills/wooyun-legacy/references/checklists/misconfig-checklist.md +124 -0
- package/skills/wooyun-legacy/references/checklists/path-traversal-checklist.md +87 -0
- package/skills/wooyun-legacy/references/checklists/rce-checklist.md +93 -0
- package/skills/wooyun-legacy/references/checklists/sql-injection-checklist.md +97 -0
- package/skills/wooyun-legacy/references/checklists/ssrf-checklist.md +99 -0
- package/skills/wooyun-legacy/references/checklists/unauthorized-access-checklist.md +89 -0
- package/skills/wooyun-legacy/references/checklists/weak-password-checklist.md +115 -0
- package/skills/wooyun-legacy/references/checklists/xss-checklist.md +103 -0
- package/skills/wooyun-legacy/references/checklists/xxe-checklist.md +130 -0
- package/skills/wooyun-legacy/references/info-disclosure.md +975 -0
- package/skills/wooyun-legacy/references/logic-flaws.md +721 -0
- package/skills/wooyun-legacy/references/path-traversal.md +1191 -0
- package/skills/wooyun-legacy/references/telecom-penetration.md +156 -0
- package/skills/wooyun-legacy/references/unauthorized-access.md +980 -0
- package/skills/wooyun-legacy/references/xss.md +746 -0
- package/skills/zeroize-audit/SKILL.md +371 -0
- package/skills/zeroize-audit/configs/c.yaml +21 -0
- package/skills/zeroize-audit/configs/default.yaml +128 -0
- package/skills/zeroize-audit/configs/rust.yaml +83 -0
- package/skills/zeroize-audit/prompts/report_template.md +238 -0
- package/skills/zeroize-audit/prompts/system.md +163 -0
- package/skills/zeroize-audit/prompts/task.md +97 -0
- package/skills/zeroize-audit/references/compile-commands.md +231 -0
- package/skills/zeroize-audit/references/detection-strategy.md +191 -0
- package/skills/zeroize-audit/references/ir-analysis.md +252 -0
- package/skills/zeroize-audit/references/mcp-analysis.md +221 -0
- package/skills/zeroize-audit/references/poc-generation.md +470 -0
- package/skills/zeroize-audit/references/rust-zeroization-patterns.md +867 -0
- package/skills/zeroize-audit/schemas/input.json +83 -0
- package/skills/zeroize-audit/schemas/output.json +140 -0
- package/skills/zeroize-audit/tools/analyze_asm.sh +202 -0
- package/skills/zeroize-audit/tools/analyze_cfg.py +381 -0
- package/skills/zeroize-audit/tools/analyze_heap.sh +211 -0
- package/skills/zeroize-audit/tools/analyze_ir_semantic.py +429 -0
- package/skills/zeroize-audit/tools/diff_ir.sh +135 -0
- package/skills/zeroize-audit/tools/diff_rust_mir.sh +189 -0
- package/skills/zeroize-audit/tools/emit_asm.sh +67 -0
- package/skills/zeroize-audit/tools/emit_ir.sh +77 -0
- package/skills/zeroize-audit/tools/emit_rust_asm.sh +178 -0
- package/skills/zeroize-audit/tools/emit_rust_ir.sh +150 -0
- package/skills/zeroize-audit/tools/emit_rust_mir.sh +158 -0
- package/skills/zeroize-audit/tools/extract_compile_flags.py +284 -0
- package/skills/zeroize-audit/tools/generate_poc.py +1329 -0
- package/skills/zeroize-audit/tools/mcp/apply_confidence_gates.py +113 -0
- package/skills/zeroize-audit/tools/mcp/check_mcp.sh +68 -0
- package/skills/zeroize-audit/tools/mcp/normalize_mcp_evidence.py +125 -0
- package/skills/zeroize-audit/tools/scripts/check_llvm_patterns.py +481 -0
- package/skills/zeroize-audit/tools/scripts/check_mir_patterns.py +554 -0
- package/skills/zeroize-audit/tools/scripts/check_rust_asm.py +424 -0
- package/skills/zeroize-audit/tools/scripts/check_rust_asm_aarch64.py +300 -0
- package/skills/zeroize-audit/tools/scripts/check_rust_asm_x86.py +283 -0
- package/skills/zeroize-audit/tools/scripts/find_dangerous_apis.py +375 -0
- package/skills/zeroize-audit/tools/scripts/semantic_audit.py +923 -0
- package/skills/zeroize-audit/tools/track_dataflow.sh +196 -0
- package/skills/zeroize-audit/tools/validate_rust_toolchain.sh +298 -0
- package/skills/zeroize-audit/workflows/phase-0-preflight.md +150 -0
- package/skills/zeroize-audit/workflows/phase-1-source-analysis.md +144 -0
- package/skills/zeroize-audit/workflows/phase-2-compiler-analysis.md +139 -0
- package/skills/zeroize-audit/workflows/phase-3-interim-report.md +46 -0
- package/skills/zeroize-audit/workflows/phase-4-poc-generation.md +46 -0
- package/skills/zeroize-audit/workflows/phase-5-poc-validation.md +136 -0
- package/skills/zeroize-audit/workflows/phase-6-final-report.md +44 -0
- package/skills/zeroize-audit/workflows/phase-7-test-generation.md +42 -0
- package/themes/piolium-srcery.json +94 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concurrency-capped FIFO scheduler used by every audit mode.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists:
|
|
5
|
+
* - Archon's Deep mode mandates a hard cap of 3 concurrently active
|
|
6
|
+
* background sub-agents (deep.md "Swarm Burst Cap"). The cap is enforced
|
|
7
|
+
* here so individual modes don't reinvent it.
|
|
8
|
+
* - Each task gets its own AbortSignal so a long-running sub-agent can be
|
|
9
|
+
* cancelled cleanly when the user aborts the audit.
|
|
10
|
+
* - Per-task timeouts catch runaway model calls without stalling the whole
|
|
11
|
+
* queue.
|
|
12
|
+
*
|
|
13
|
+
* The scheduler is intentionally tiny — no priority levels, no retries (let
|
|
14
|
+
* the caller decide policy), no work-stealing. It only does:
|
|
15
|
+
*
|
|
16
|
+
* 1. honour `maxConcurrent`
|
|
17
|
+
* 2. process tasks FIFO
|
|
18
|
+
* 3. propagate abort and timeout via AbortSignal
|
|
19
|
+
*
|
|
20
|
+
* Transcript dirs (`piolium/tmp/piolium/runs/<id>/`) are exposed as helpers
|
|
21
|
+
* so the agent runner (M2) writes to a stable, scheduler-known location.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
|
|
27
|
+
export interface SchedulerOptions {
|
|
28
|
+
/** Maximum simultaneous in-flight tasks. Defaults to 3. */
|
|
29
|
+
maxConcurrent?: number;
|
|
30
|
+
/** External abort signal; aborting this aborts the scheduler and all tasks. */
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ScheduledTask<T> {
|
|
35
|
+
/** Stable identifier — surfaced in run dirs and logs. */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Optional human label for status widgets. */
|
|
38
|
+
label?: string;
|
|
39
|
+
/** Per-task timeout in milliseconds. Omit for no timeout. */
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Task body. Receives an AbortSignal that fires on either external abort
|
|
43
|
+
* or timeout. Should reject promptly when the signal fires.
|
|
44
|
+
*/
|
|
45
|
+
run: (signal: AbortSignal) => Promise<T>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SchedulerStats {
|
|
49
|
+
maxConcurrent: number;
|
|
50
|
+
active: number;
|
|
51
|
+
pending: number;
|
|
52
|
+
completed: number;
|
|
53
|
+
failed: number;
|
|
54
|
+
aborted: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface PendingEntry<T> {
|
|
58
|
+
task: ScheduledTask<T>;
|
|
59
|
+
resolve: (value: T) => void;
|
|
60
|
+
reject: (err: unknown) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class SchedulerAbortError extends Error {
|
|
64
|
+
constructor(message = "Scheduler aborted") {
|
|
65
|
+
super(message);
|
|
66
|
+
this.name = "SchedulerAbortError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class TaskTimeoutError extends Error {
|
|
71
|
+
constructor(
|
|
72
|
+
public readonly taskId: string,
|
|
73
|
+
public readonly timeoutMs: number,
|
|
74
|
+
) {
|
|
75
|
+
super(`Task ${taskId} timed out after ${timeoutMs}ms`);
|
|
76
|
+
this.name = "TaskTimeoutError";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class Scheduler {
|
|
81
|
+
readonly maxConcurrent: number;
|
|
82
|
+
private active = 0;
|
|
83
|
+
private completed = 0;
|
|
84
|
+
private failed = 0;
|
|
85
|
+
private aborted = false;
|
|
86
|
+
private readonly queue: PendingEntry<unknown>[] = [];
|
|
87
|
+
private readonly inflight = new Set<AbortController>();
|
|
88
|
+
private readonly externalSignal?: AbortSignal;
|
|
89
|
+
private readonly externalAbortListener?: () => void;
|
|
90
|
+
|
|
91
|
+
constructor(opts: SchedulerOptions = {}) {
|
|
92
|
+
this.maxConcurrent = Math.max(1, opts.maxConcurrent ?? 3);
|
|
93
|
+
this.externalSignal = opts.signal;
|
|
94
|
+
if (this.externalSignal) {
|
|
95
|
+
if (this.externalSignal.aborted) {
|
|
96
|
+
this.aborted = true;
|
|
97
|
+
} else {
|
|
98
|
+
this.externalAbortListener = () => this.abort();
|
|
99
|
+
this.externalSignal.addEventListener("abort", this.externalAbortListener, { once: true });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
enqueue<T>(task: ScheduledTask<T>): Promise<T> {
|
|
105
|
+
if (this.aborted) {
|
|
106
|
+
return Promise.reject(new SchedulerAbortError());
|
|
107
|
+
}
|
|
108
|
+
return new Promise<T>((resolve, reject) => {
|
|
109
|
+
this.queue.push({
|
|
110
|
+
task: task as ScheduledTask<unknown>,
|
|
111
|
+
resolve: resolve as (v: unknown) => void,
|
|
112
|
+
reject,
|
|
113
|
+
});
|
|
114
|
+
this.pump();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convenience: enqueue many tasks and wait for all settlements (mirrors
|
|
120
|
+
* Promise.allSettled). The cap still applies — only `maxConcurrent` are
|
|
121
|
+
* in-flight at once even when callers pass a giant array.
|
|
122
|
+
*/
|
|
123
|
+
async runBatch<T>(tasks: ScheduledTask<T>[]): Promise<PromiseSettledResult<T>[]> {
|
|
124
|
+
return Promise.allSettled(tasks.map((t) => this.enqueue(t)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
abort(): void {
|
|
128
|
+
if (this.aborted) return;
|
|
129
|
+
this.aborted = true;
|
|
130
|
+
// Reject everything still queued so callers stop waiting.
|
|
131
|
+
const drain = this.queue.splice(0, this.queue.length);
|
|
132
|
+
for (const entry of drain) entry.reject(new SchedulerAbortError());
|
|
133
|
+
// Cancel everything currently running.
|
|
134
|
+
for (const ctrl of this.inflight) ctrl.abort(new SchedulerAbortError());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
stats(): SchedulerStats {
|
|
138
|
+
return {
|
|
139
|
+
maxConcurrent: this.maxConcurrent,
|
|
140
|
+
active: this.active,
|
|
141
|
+
pending: this.queue.length,
|
|
142
|
+
completed: this.completed,
|
|
143
|
+
failed: this.failed,
|
|
144
|
+
aborted: this.aborted,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
dispose(): void {
|
|
149
|
+
this.abort();
|
|
150
|
+
if (this.externalSignal && this.externalAbortListener) {
|
|
151
|
+
this.externalSignal.removeEventListener("abort", this.externalAbortListener);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private pump(): void {
|
|
156
|
+
while (!this.aborted && this.active < this.maxConcurrent && this.queue.length > 0) {
|
|
157
|
+
const entry = this.queue.shift();
|
|
158
|
+
if (!entry) break;
|
|
159
|
+
void this.runEntry(entry);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async runEntry(entry: PendingEntry<unknown>): Promise<void> {
|
|
164
|
+
this.active++;
|
|
165
|
+
const ctrl = new AbortController();
|
|
166
|
+
this.inflight.add(ctrl);
|
|
167
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
168
|
+
if (entry.task.timeoutMs && entry.task.timeoutMs > 0) {
|
|
169
|
+
const timeoutMs = entry.task.timeoutMs;
|
|
170
|
+
timeoutHandle = setTimeout(() => {
|
|
171
|
+
ctrl.abort(new TaskTimeoutError(entry.task.id, timeoutMs));
|
|
172
|
+
}, timeoutMs);
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
const result = await entry.task.run(ctrl.signal);
|
|
176
|
+
this.completed++;
|
|
177
|
+
entry.resolve(result);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
this.failed++;
|
|
180
|
+
// Surface the timeout reason as the rejected error so callers can
|
|
181
|
+
// distinguish abort from "task threw on its own".
|
|
182
|
+
if (ctrl.signal.aborted && ctrl.signal.reason instanceof TaskTimeoutError) {
|
|
183
|
+
entry.reject(ctrl.signal.reason);
|
|
184
|
+
} else {
|
|
185
|
+
entry.reject(err);
|
|
186
|
+
}
|
|
187
|
+
} finally {
|
|
188
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
189
|
+
this.inflight.delete(ctrl);
|
|
190
|
+
this.active--;
|
|
191
|
+
this.pump();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Path conventions for per-task transcript directories. Modes use these to
|
|
198
|
+
* keep raw subagent output isolated from final artifacts under `piolium/`.
|
|
199
|
+
*/
|
|
200
|
+
export function getRunsRoot(cwd: string): string {
|
|
201
|
+
return join(cwd, "piolium", "tmp", "piolium", "runs");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getRunDir(cwd: string, runId: string): string {
|
|
205
|
+
return join(getRunsRoot(cwd), runId);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function ensureRunDir(cwd: string, runId: string): string {
|
|
209
|
+
const dir = getRunDir(cwd, runId);
|
|
210
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
211
|
+
return dir;
|
|
212
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Q1 secrets scanner.
|
|
3
|
+
*
|
|
4
|
+
* Tries `trufflehog`, then `gitleaks`, then a regex-grep fallback. Each
|
|
5
|
+
* finding is materialised as `piolium/findings-draft/q1-<NNN>-<slug>.md` so
|
|
6
|
+
* later phases (consolidation, finalisation) can pick them up using the same
|
|
7
|
+
* conventions as the upstream archon-audit pipeline.
|
|
8
|
+
*
|
|
9
|
+
* Tool detection is best-effort: when a binary isn't on PATH the next
|
|
10
|
+
* fallback is tried and a note is left in the phase summary. We never throw
|
|
11
|
+
* just because a tool is missing — that's expected on slim CI runners.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
15
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
export type SecretsBackend = "trufflehog" | "gitleaks" | "grep" | "none";
|
|
19
|
+
|
|
20
|
+
export interface SecretFinding {
|
|
21
|
+
id: string;
|
|
22
|
+
slug: string;
|
|
23
|
+
severity: "high" | "medium" | "low";
|
|
24
|
+
title: string;
|
|
25
|
+
file: string;
|
|
26
|
+
line?: number;
|
|
27
|
+
rule?: string;
|
|
28
|
+
excerpt?: string;
|
|
29
|
+
source: SecretsBackend;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SecretsScanResult {
|
|
33
|
+
backend: SecretsBackend;
|
|
34
|
+
findings: SecretFinding[];
|
|
35
|
+
notes: string[];
|
|
36
|
+
draftPaths: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const Q1_SECRETS_SUMMARY = "piolium/attack-surface/lite-q1-summary.md";
|
|
40
|
+
|
|
41
|
+
function which(bin: string): boolean {
|
|
42
|
+
try {
|
|
43
|
+
const res = spawnSync("command", ["-v", bin], { stdio: "ignore", shell: true });
|
|
44
|
+
return res.status === 0;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safeExec(file: string, args: string[], cwd: string): string | undefined {
|
|
51
|
+
try {
|
|
52
|
+
return execFileSync(file, args, {
|
|
53
|
+
cwd,
|
|
54
|
+
encoding: "utf8",
|
|
55
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
56
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const e = err as NodeJS.ErrnoException & { stdout?: string };
|
|
60
|
+
// Many secret-scanning tools exit non-zero specifically *because* they
|
|
61
|
+
// found something — they still print results to stdout.
|
|
62
|
+
if (typeof e.stdout === "string" && e.stdout.length > 0) return e.stdout;
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function slugify(text: string): string {
|
|
68
|
+
return (
|
|
69
|
+
text
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
72
|
+
.replace(/^-+|-+$/g, "")
|
|
73
|
+
.slice(0, 60) || "secret"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function severityFromRule(rule: string): "high" | "medium" | "low" {
|
|
78
|
+
const lower = rule.toLowerCase();
|
|
79
|
+
if (
|
|
80
|
+
lower.includes("private") ||
|
|
81
|
+
lower.includes("aws") ||
|
|
82
|
+
lower.includes("gcp") ||
|
|
83
|
+
lower.includes("azure")
|
|
84
|
+
)
|
|
85
|
+
return "high";
|
|
86
|
+
if (lower.includes("token") || lower.includes("api") || lower.includes("password"))
|
|
87
|
+
return "medium";
|
|
88
|
+
return "low";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface TruffleHogRecord {
|
|
92
|
+
DetectorName?: string;
|
|
93
|
+
Verified?: boolean;
|
|
94
|
+
Raw?: string;
|
|
95
|
+
SourceMetadata?: {
|
|
96
|
+
Data?: {
|
|
97
|
+
Filesystem?: { file?: string; line?: number };
|
|
98
|
+
Git?: { file?: string; line?: number };
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function parseTrufflehog(stdout: string): SecretFinding[] {
|
|
104
|
+
const findings: SecretFinding[] = [];
|
|
105
|
+
let counter = 0;
|
|
106
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
107
|
+
if (!line.trim()) continue;
|
|
108
|
+
try {
|
|
109
|
+
const rec = JSON.parse(line) as TruffleHogRecord;
|
|
110
|
+
const detector = rec.DetectorName ?? "unknown";
|
|
111
|
+
const fsMeta = rec.SourceMetadata?.Data?.Filesystem;
|
|
112
|
+
const gitMeta = rec.SourceMetadata?.Data?.Git;
|
|
113
|
+
const file = fsMeta?.file ?? gitMeta?.file ?? "(unknown)";
|
|
114
|
+
const lineNum = fsMeta?.line ?? gitMeta?.line;
|
|
115
|
+
counter++;
|
|
116
|
+
const id = `q1-${String(counter).padStart(3, "0")}`;
|
|
117
|
+
findings.push({
|
|
118
|
+
id,
|
|
119
|
+
slug: slugify(`${detector}-${file}`),
|
|
120
|
+
severity: rec.Verified ? "high" : severityFromRule(detector),
|
|
121
|
+
title: `${detector} secret detected (${rec.Verified ? "verified" : "unverified"})`,
|
|
122
|
+
file,
|
|
123
|
+
...(lineNum ? { line: lineNum } : {}),
|
|
124
|
+
rule: detector,
|
|
125
|
+
...(rec.Raw ? { excerpt: rec.Raw.slice(0, 200) } : {}),
|
|
126
|
+
source: "trufflehog",
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
// non-JSON lines (banners, errors) are ignored
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return findings;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface GitleaksRecord {
|
|
136
|
+
Description?: string;
|
|
137
|
+
File?: string;
|
|
138
|
+
StartLine?: number;
|
|
139
|
+
Match?: string;
|
|
140
|
+
RuleID?: string;
|
|
141
|
+
Secret?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseGitleaks(stdout: string): SecretFinding[] {
|
|
145
|
+
let arr: GitleaksRecord[];
|
|
146
|
+
try {
|
|
147
|
+
arr = JSON.parse(stdout) as GitleaksRecord[];
|
|
148
|
+
if (!Array.isArray(arr)) return [];
|
|
149
|
+
} catch {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
return arr.map((r, i) => {
|
|
153
|
+
const counter = i + 1;
|
|
154
|
+
const file = r.File ?? "(unknown)";
|
|
155
|
+
const rule = r.RuleID ?? r.Description ?? "gitleaks-rule";
|
|
156
|
+
return {
|
|
157
|
+
id: `q1-${String(counter).padStart(3, "0")}`,
|
|
158
|
+
slug: slugify(`${rule}-${file}`),
|
|
159
|
+
severity: severityFromRule(rule),
|
|
160
|
+
title: r.Description ?? "gitleaks finding",
|
|
161
|
+
file,
|
|
162
|
+
...(r.StartLine ? { line: r.StartLine } : {}),
|
|
163
|
+
rule,
|
|
164
|
+
...(r.Match ? { excerpt: r.Match.slice(0, 200) } : {}),
|
|
165
|
+
source: "gitleaks" as const,
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const GREP_PATTERNS: Array<{ rule: string; regex: RegExp; severity: "high" | "medium" | "low" }> = [
|
|
171
|
+
{
|
|
172
|
+
rule: "aws-access-key-id",
|
|
173
|
+
regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g,
|
|
174
|
+
severity: "high",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
rule: "private-key-pem",
|
|
178
|
+
regex: /-----BEGIN (?:RSA|EC|DSA|OPENSSH|PRIVATE) (?:PRIVATE )?KEY-----/g,
|
|
179
|
+
severity: "high",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
rule: "github-token",
|
|
183
|
+
regex: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g,
|
|
184
|
+
severity: "high",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
rule: "slack-token",
|
|
188
|
+
regex: /\bxox[baprs]-[A-Za-z0-9-]{10,48}\b/g,
|
|
189
|
+
severity: "medium",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
rule: "generic-api-key",
|
|
193
|
+
regex: /(?:api[_-]?key|secret|password|token)\s*[:=]\s*['"][A-Za-z0-9+/=_-]{16,}['"]/gi,
|
|
194
|
+
severity: "low",
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
const GREP_INCLUDE = [
|
|
199
|
+
"*.ts",
|
|
200
|
+
"*.tsx",
|
|
201
|
+
"*.js",
|
|
202
|
+
"*.jsx",
|
|
203
|
+
"*.py",
|
|
204
|
+
"*.go",
|
|
205
|
+
"*.rs",
|
|
206
|
+
"*.rb",
|
|
207
|
+
"*.java",
|
|
208
|
+
"*.kt",
|
|
209
|
+
"*.cs",
|
|
210
|
+
"*.php",
|
|
211
|
+
"*.sh",
|
|
212
|
+
"*.bash",
|
|
213
|
+
"*.env",
|
|
214
|
+
"*.yml",
|
|
215
|
+
"*.yaml",
|
|
216
|
+
"*.toml",
|
|
217
|
+
"*.ini",
|
|
218
|
+
"*.cfg",
|
|
219
|
+
"*.conf",
|
|
220
|
+
"*.json",
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
function runGrepFallback(cwd: string): SecretFinding[] {
|
|
224
|
+
const findings: SecretFinding[] = [];
|
|
225
|
+
let counter = 0;
|
|
226
|
+
for (const { rule, regex, severity } of GREP_PATTERNS) {
|
|
227
|
+
const args = [
|
|
228
|
+
"-rEn",
|
|
229
|
+
"--binary-files=without-match",
|
|
230
|
+
"--exclude-dir=node_modules",
|
|
231
|
+
"--exclude-dir=.git",
|
|
232
|
+
"--exclude-dir=vendor",
|
|
233
|
+
"--exclude-dir=dist",
|
|
234
|
+
"--exclude-dir=build",
|
|
235
|
+
"--exclude-dir=piolium",
|
|
236
|
+
...GREP_INCLUDE.flatMap((g) => ["--include", g]),
|
|
237
|
+
regex.source,
|
|
238
|
+
".",
|
|
239
|
+
];
|
|
240
|
+
const stdout = safeExec("grep", args, cwd);
|
|
241
|
+
if (!stdout) continue;
|
|
242
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
243
|
+
if (!line.trim()) continue;
|
|
244
|
+
// Format: ./path/to/file:42:matched text
|
|
245
|
+
const m = line.match(/^(?:\.\/)?(.+?):(\d+):(.*)$/);
|
|
246
|
+
if (!m) continue;
|
|
247
|
+
const file = m[1] ?? "(unknown)";
|
|
248
|
+
const lineNum = Number(m[2]);
|
|
249
|
+
const excerpt = (m[3] ?? "").trim().slice(0, 200);
|
|
250
|
+
counter++;
|
|
251
|
+
findings.push({
|
|
252
|
+
id: `q1-${String(counter).padStart(3, "0")}`,
|
|
253
|
+
slug: slugify(`${rule}-${file}`),
|
|
254
|
+
severity,
|
|
255
|
+
title: `${rule} match`,
|
|
256
|
+
file,
|
|
257
|
+
...(Number.isFinite(lineNum) ? { line: lineNum } : {}),
|
|
258
|
+
rule,
|
|
259
|
+
...(excerpt ? { excerpt } : {}),
|
|
260
|
+
source: "grep" as const,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return findings;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function findingMarkdown(f: SecretFinding): string {
|
|
268
|
+
return [
|
|
269
|
+
"---",
|
|
270
|
+
`id: ${f.id}`,
|
|
271
|
+
"phase: Q1",
|
|
272
|
+
`slug: ${f.slug}`,
|
|
273
|
+
`severity: ${f.severity}`,
|
|
274
|
+
`source: ${f.source}`,
|
|
275
|
+
`rule: ${f.rule ?? "(none)"}`,
|
|
276
|
+
"---",
|
|
277
|
+
"",
|
|
278
|
+
`# ${f.title}`,
|
|
279
|
+
"",
|
|
280
|
+
`- File: \`${f.file}\``,
|
|
281
|
+
f.line ? `- Line: ${f.line}` : "",
|
|
282
|
+
f.excerpt ? "" : "",
|
|
283
|
+
f.excerpt ? `## Excerpt\n\n\`\`\`\n${f.excerpt}\n\`\`\`` : "",
|
|
284
|
+
"",
|
|
285
|
+
"## Notes",
|
|
286
|
+
"",
|
|
287
|
+
"This is a draft finding produced by the Q1 secrets scan. Confirm by inspecting the file in context.",
|
|
288
|
+
"",
|
|
289
|
+
]
|
|
290
|
+
.filter((s) => s !== "")
|
|
291
|
+
.join("\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function findingsDraftDir(cwd: string): string {
|
|
295
|
+
return join(cwd, "piolium", "findings-draft");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function runQ1SecretsScan(cwd: string): SecretsScanResult {
|
|
299
|
+
const notes: string[] = [];
|
|
300
|
+
let backend: SecretsBackend = "none";
|
|
301
|
+
let findings: SecretFinding[] = [];
|
|
302
|
+
|
|
303
|
+
if (which("trufflehog")) {
|
|
304
|
+
notes.push("Backend: trufflehog (filesystem mode).");
|
|
305
|
+
const out = safeExec(
|
|
306
|
+
"trufflehog",
|
|
307
|
+
["filesystem", "--json", "--no-update", "--exclude-paths=piolium", "."],
|
|
308
|
+
cwd,
|
|
309
|
+
);
|
|
310
|
+
if (out) {
|
|
311
|
+
findings = parseTrufflehog(out);
|
|
312
|
+
backend = "trufflehog";
|
|
313
|
+
} else {
|
|
314
|
+
notes.push("trufflehog returned no output — falling back.");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (backend === "none" && which("gitleaks")) {
|
|
318
|
+
notes.push("Backend: gitleaks (no-git mode).");
|
|
319
|
+
const tmp = join(cwd, "piolium", "tmp", "piolium", "gitleaks.json");
|
|
320
|
+
mkdirSync(join(cwd, "piolium", "tmp", "piolium"), { recursive: true });
|
|
321
|
+
safeExec(
|
|
322
|
+
"gitleaks",
|
|
323
|
+
["detect", "--source", ".", "--no-git", "--report-format", "json", "--report-path", tmp],
|
|
324
|
+
cwd,
|
|
325
|
+
);
|
|
326
|
+
if (existsSync(tmp)) {
|
|
327
|
+
try {
|
|
328
|
+
const txt = require("node:fs").readFileSync(tmp, "utf8") as string;
|
|
329
|
+
findings = parseGitleaks(txt);
|
|
330
|
+
backend = "gitleaks";
|
|
331
|
+
} catch {
|
|
332
|
+
notes.push("gitleaks report unreadable.");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (backend === "none") {
|
|
337
|
+
notes.push("Backend: regex grep fallback (no trufflehog or gitleaks on PATH).");
|
|
338
|
+
findings = runGrepFallback(cwd);
|
|
339
|
+
backend = "grep";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const draftDir = findingsDraftDir(cwd);
|
|
343
|
+
mkdirSync(draftDir, { recursive: true });
|
|
344
|
+
const draftPaths: string[] = [];
|
|
345
|
+
for (const f of findings) {
|
|
346
|
+
const path = join(draftDir, `${f.id}-${f.slug}.md`);
|
|
347
|
+
writeFileSync(path, findingMarkdown(f));
|
|
348
|
+
draftPaths.push(path);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Summary placeholder so the gate has something to read even when no
|
|
352
|
+
// findings surface — distinguishes "scan ran, clean" from "scan never ran".
|
|
353
|
+
const summaryPath = join(cwd, Q1_SECRETS_SUMMARY);
|
|
354
|
+
mkdirSync(join(cwd, "piolium", "attack-surface"), { recursive: true });
|
|
355
|
+
writeFileSync(
|
|
356
|
+
summaryPath,
|
|
357
|
+
[
|
|
358
|
+
"# Q1 Secrets Scan",
|
|
359
|
+
"",
|
|
360
|
+
`Backend: ${backend}`,
|
|
361
|
+
`Findings: ${findings.length}`,
|
|
362
|
+
notes.length > 0 ? `\nNotes:\n${notes.map((n) => `- ${n}`).join("\n")}` : "",
|
|
363
|
+
"",
|
|
364
|
+
].join("\n"),
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
return { backend, findings, notes, draftPaths };
|
|
368
|
+
}
|