@gpzhang2001/sharpkit-reporting 0.2.1 → 0.2.2

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/lib/index.d.ts CHANGED
@@ -97,6 +97,7 @@ var [ReportState] = [
97
97
  "",
98
98
  "",
99
99
  "",
100
+ "",
100
101
  ""
101
102
  ]
102
103
  ];
@@ -440,6 +441,9 @@ var [ReportingHandle] = [
440
441
  () => [
441
442
  ReportState,
442
443
  Promise,
444
+ Record,
445
+ Readonly,
446
+ Promise,
443
447
  Promise,
444
448
  Promise
445
449
  ],
@@ -459,6 +463,12 @@ var [ReportingHandle] = [
459
463
  "",
460
464
  "",
461
465
  "",
466
+ "",
467
+ "",
468
+ "",
469
+ "",
470
+ "",
471
+ "",
462
472
  ""
463
473
  ]
464
474
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"index.ts","names":["Schema"],"sources":["../src/state.d.ts","../src/dedupe.d.ts","../src/writers.d.ts","../src/cvss.d.ts","../src/sarif.d.ts","../src/index.d.ts"],"sourcesContent":["/**\n * Report state — port of strix report/state.py: the per-scan report store\n * with sequential `vuln-NNNN` ids, strix's exact field insertion order,\n * title cleaning, update whitelist with dependent-field dropping, update\n * history, and hydration from a previous run dir. Key order matters: the\n * stored dicts are JSON-serialized byte-for-byte into vulnerabilities.json,\n * so fields are inserted in state.py `add_vulnerability_report` order.\n * @module @gpzhang2001/sharpkit-reporting/state\n */\n/** A stored vulnerability/dependency report dict (strix shape). */\nexport interface VulnerabilityReport {\n readonly id: string;\n title: string;\n severity: string;\n readonly timestamp: string;\n [key: string]: unknown;\n}\n/** Injectable clock (tests pass fixed times; default wall clock). */\nexport type Clock = () => Date;\n/** strix display timestamp format (state.py :349). */\nexport declare function formatTimestamp(date: Date): string;\n/** strix ISO instant format (start/end times). */\nexport declare function formatIso(date: Date): string;\n/** Control-char → space + whitespace collapse (state.py `_clean_title`). */\nexport declare function cleanTitle(title: string): string;\n/** strix severity order (tool.py `_SEVERITY_ORDER`). */\nexport declare const SEVERITY_ORDER: readonly [\"critical\", \"high\", \"medium\", \"low\", \"info\", \"none\"];\n/** Severity rank with unknown → last (writer parity). */\nexport declare function severityRank(severity: string): number;\n/** Updatable field whitelist (state.py `UPDATABLE_REPORT_FIELDS`). */\nexport declare const UPDATABLE_REPORT_FIELDS: Set<string>;\n/** Dependent fields dropped when their primary changes without replacement. */\nexport declare const DEPENDENT_REPORT_FIELDS: Readonly<Record<string, string>>;\n/** One update-history entry (state.py :467-487). */\nexport interface UpdateHistoryEntry {\n readonly timestamp: string;\n fields: string[];\n dropped_fields?: string[];\n reason?: string;\n agent_id?: string;\n agent_name?: string;\n previous_severity?: string;\n previous_cvss?: number;\n previous_confidence?: string;\n}\nexport interface AddReportInput {\n readonly title: string;\n readonly severity: string;\n readonly findingClass?: string | undefined;\n readonly dependencyMetadata?: Record<string, unknown> | undefined;\n readonly agentId?: string | undefined;\n readonly agentName?: string | undefined;\n /** All remaining report fields (already validated/normalized by the tool layer). */\n readonly fields: Record<string, unknown>;\n}\n/** The revision outcome: null = no-op (strix parity). */\nexport type UpdateOutcome = {\n readonly report: VulnerabilityReport;\n} | {\n readonly noop: true;\n};\n/**\n * One scan's report store. Not a process singleton — the suite runs one per\n * scan id inside the tool package's closure (strix's module-global maps to a\n * per-scan instance in dsh).\n */\nexport declare class ReportState {\n readonly runId: string;\n runName: string | null;\n readonly startTime: string;\n endTime: string | null;\n status: string;\n finalScanResult: string | null;\n readonly vulnerabilityReports: VulnerabilityReport[];\n /** Ids already rendered to markdown (incremental writer input). */\n readonly savedVulnIds: Set<string>;\n updateHistoryAgent: {\n readonly agentId?: string;\n readonly agentName?: string;\n } | undefined;\n private readonly clock;\n constructor(options?: {\n readonly runId?: string;\n readonly runName?: string | null;\n readonly clock?: Clock;\n });\n /** Allocate the next sequential id (state.py :343 — length-derived). */\n private nextId;\n /**\n * Add one report with strix's exact field construction order.\n * @param input - the validated/normalized create payload.\n */\n addVulnerabilityReport(input: AddReportInput): VulnerabilityReport;\n /**\n * Revise one report in place (state.py `update_vulnerability_report`):\n * whitelist + strip/lowercase, unchanged-skip, dependent-field dropping,\n * history append, and the MD invalidation marker.\n * @param reportId - the `vuln-NNNN` id.\n * @param changes - only the fields the model passed.\n * @param reason - the update_reason (truncated to 500).\n */\n updateVulnerabilityReport(reportId: string, changes: Record<string, unknown>, reason: string): UpdateOutcome;\n /**\n * Reload from a previous run's vulnerabilities.json so id allocation does\n * not collide (state.py `hydrate_from_run_dir`; raises on corrupt JSON).\n * @param reports - the parsed JSON array.\n */\n hydrate(reports: unknown): void;\n /** Mark the run complete (status transition + end time). */\n complete(exitStatus?: string): void;\n}\n","/**\n * Dedupe — port of strix report/dedupe.py decision flow: the deterministic\n * dependency identity fast path (CVE × package × ecosystem, distinct\n * manifests are separate findings, legacy word-bounded mention fallback) and\n * an injectable LLM judge for dynamic findings. Every judge failure defaults\n * to NOT duplicate (strix parity: dedupe failures never block a finding).\n * @module @gpzhang2001/sharpkit-reporting/dedupe\n */\nimport type { VulnerabilityReport } from './state.ts';\n/** Dependency identity fields (strix `_dependency_identity`). */\nexport interface DependencyIdentity {\n readonly cve: string;\n readonly packageName: string;\n readonly ecosystem: string;\n}\n/** The dedupe verdict shape (strix `DuplicateCheckResult`). */\nexport interface DuplicateVerdict {\n readonly isDuplicate: boolean;\n readonly duplicateId: string;\n readonly confidence: number;\n readonly reason: string;\n}\n/** Pluggable LLM judge (Config-injected; dsh-side LLM wiring lands with M4). */\nexport type DedupeJudge = (candidate: Record<string, unknown>, existing: readonly VulnerabilityReport[]) => Promise<DuplicateVerdict>;\n/** Extract the dependency identity from metadata (strix :162-177). */\nexport declare function dependencyIdentity(metadata: unknown): DependencyIdentity | null;\n/** Whether two manifest paths are both present AND different (strix `_distinct_manifest_paths`). */\nexport declare function distinctManifestPaths(a: unknown, b: unknown): boolean;\n/** Word-bounded regex mention check over prose fields (strix `_legacy_report_mentions_package`). */\nexport declare function legacyReportMentionsPackage(report: VulnerabilityReport, identity: DependencyIdentity): boolean;\n/**\n * The deterministic dependency fast path (strix `_check_dependency_duplicate`).\n * @param candidateIdentity - the candidate's identity.\n * @param candidateMetadata - the candidate's dependency metadata (manifest comparison).\n * @param existing - current reports.\n * @returns a verdict, or null to defer to the LLM judge.\n */\nexport declare function checkDependencyDuplicate(candidateIdentity: DependencyIdentity, candidateMetadata: Record<string, unknown> | undefined, existing: readonly VulnerabilityReport[]): DuplicateVerdict | null;\n/**\n * Entry point (strix `check_duplicate`): fast path for dependency\n * candidates, then the injected judge; every judge failure is NOT duplicate.\n * @param candidate - the candidate fields sent for comparison.\n * @param candidateMetadata - dependency metadata when present.\n * @param existing - current reports.\n * @param judge - the injected LLM judge (absent → not duplicate).\n */\nexport declare function checkDuplicate(candidate: Record<string, unknown>, candidateMetadata: Record<string, unknown> | undefined, existing: readonly VulnerabilityReport[], judge: DedupeJudge | undefined): Promise<DuplicateVerdict>;\n","/**\n * Artifact writers — port of strix report/writer.py: atomic writes\n * (temp-in-same-dir + rename, no fsync), run.json, vulnerabilities.csv\n * (formula-injection guard, \\r\\n terminators, uppercase severity,\n * severity-then-timestamp ordering), vulnerabilities.json (byte-identical\n * serialization), the vuln-NNNN.md renderer (exact section order), and the\n * executive report template.\n * @module @gpzhang2001/sharpkit-reporting/writers\n */\nimport { type VulnerabilityReport } from './state.ts';\n/** JSON.stringify with Python `json.dumps(ensure_ascii=False, indent=2)` parity. */\nexport declare function dumpsIndent(value: unknown): string;\n/**\n * Atomic text write (writer.py `atomic_write_text` :201-220): temp file in\n * the target's directory + rename; no fsync (strix parity).\n * @param path - target file path.\n * @param payload - exact bytes to write.\n */\nexport declare function atomicWriteText(path: string, payload: string): Promise<void>;\n/**\n * Formula-injection guard (writer.py `csv_safe` :35-53): prefix `'` when the\n * rendered cell starts with `= + - @ \\t \\r`.\n * @param value - the cell value.\n */\nexport declare function csvSafe(value: string): string;\n/**\n * Render vulnerabilities.csv: header + rows sorted by (severity rank,\n * timestamp), uppercase severity, \\r\\n terminators.\n * @param reports - the stored reports.\n */\nexport declare function renderVulnerabilitiesCsv(reports: readonly VulnerabilityReport[]): string;\n/** Update history section (writer.py `render_update_history` :364-396). */\nexport declare function renderUpdateHistory(report: Record<string, unknown>): string[];\n/**\n * Render one vulnerability markdown document (writer.py\n * `render_vulnerability_md` :223-361 section order).\n * @param report - the stored report dict.\n */\nexport declare function renderVulnerabilityMd(report: VulnerabilityReport): string;\n/**\n * Write the per-run artifacts handled outside SARIF (writer.py parity):\n * vulnerabilities/*.md (incremental), vulnerabilities.csv,\n * vulnerabilities.json.\n * @param runDir - the run directory.\n * @param reports - all stored reports.\n * @param savedIds - ids whose MD is already on disk (updated ids must be re-rendered by the caller removing them).\n */\nexport declare function writeVulnerabilities(runDir: string, reports: readonly VulnerabilityReport[], savedIds: ReadonlySet<string>): Promise<void>;\n/**\n * Write the assembled coverage document (coverage.py `write_coverage`).\n * @param runDir - the run directory.\n * @param document - the assembled coverage document.\n */\nexport declare function writeCoverage(runDir: string, document: Record<string, unknown>): Promise<void>;\n/**\n * Write run.json last (state.py ordering).\n * @param runDir - the run directory.\n * @param runRecord - the run record dict.\n */\nexport declare function writeRunRecord(runDir: string, runRecord: Record<string, unknown>): Promise<void>;\n/**\n * Write the executive report (plain truncate-write, writer.py :139-145).\n * @param runDir - the run directory.\n * @param finalScanResult - the composed final report body.\n * @param generatedAt - display timestamp.\n */\nexport declare function writeExecutiveReport(runDir: string, finalScanResult: string, generatedAt: string): Promise<void>;\n/** Severity list used by callers that need the canonical order. */\nexport declare const severityOrder: readonly [\"critical\", \"high\", \"medium\", \"low\", \"info\", \"none\"];\n","/**\n * CVSS v3.1 base-score math, ported from strix's usage of the `cvss`\n * package (tool.py `_calculate_cvss` :134-153): build the vector string,\n * compute the base score and qualitative severity. The score uses the\n * CVSS 3.1 spec formula with the spec's Roundup1 (ceil to one decimal with\n * the IEEE-754 guard), and a \"none\" base severity is remapped to \"info\"\n * (strix parity).\n * @module @gpzhang2001/sharpkit-reporting/cvss\n */\n/** The eight CVSS metrics with their allowed values (strix `_CVSS_VALID`). */\nexport declare const CVSS_VALID: {\n readonly attack_vector: readonly [\"N\", \"A\", \"L\", \"P\"];\n readonly attack_complexity: readonly [\"L\", \"H\"];\n readonly privileges_required: readonly [\"N\", \"L\", \"H\"];\n readonly user_interaction: readonly [\"N\", \"R\"];\n readonly scope: readonly [\"U\", \"C\"];\n readonly confidentiality: readonly [\"N\", \"L\", \"H\"];\n readonly integrity: readonly [\"N\", \"L\", \"H\"];\n readonly availability: readonly [\"N\", \"L\", \"H\"];\n};\nexport type CvssMetricName = keyof typeof CVSS_VALID;\n/**\n * Validate a breakdown against the allowed metric/value sets.\n * @param breakdown - the model-supplied 8-metric dict.\n * @returns validation errors, empty when valid.\n */\nexport declare function validateCvssBreakdown(breakdown: Record<string, unknown>): string[];\n/**\n * Build the vector string (strix format: CVSS:3.1/AV:../AC:../PR:../UI:../S:../C:../I:../A:..).\n * @param breakdown - a validated breakdown.\n */\nexport declare function buildCvssVector(breakdown: Readonly<Record<CvssMetricName, string>>): string;\n/**\n * Compute the CVSS v3.1 base score for a breakdown (the `cvss` package's\n * `compute_base_score`: Scope U ISC = 6.42×ISCBase; Scope C ISC = the 7.52\n * formula; Scope C multiplies the total by 1.08 before the cap).\n * @param breakdown - a validated breakdown.\n * @returns the rounded base score (0.0 when impact is non-positive).\n */\nexport declare function cvssBaseScore(breakdown: Readonly<Record<CvssMetricName, string>>): number;\n/**\n * Qualitative severity banding (the `cvss` package's rating bands).\n * @param score - the base score.\n */\nexport declare function cvssSeverity(score: number): 'none' | 'low' | 'medium' | 'high' | 'critical';\n/**\n * Full computation: vector + score + severity with the \"none\"→\"info\" remap\n * (strix `_calculate_cvss` return contract).\n * @param breakdown - a validated breakdown.\n */\nexport declare function calculateCvss(breakdown: Readonly<Record<CvssMetricName, string>>): {\n readonly vector: string;\n readonly score: number;\n readonly severity: string;\n};\n/**\n * Dependency severity banding from an advisory score (strix\n * `_DEP_SEVERITY_FROM_CVSS` :1363-1378, top band inclusive at 10.0).\n * @param score - the advisory base score.\n */\nexport declare function dependencySeverity(score: number): string;\n","/**\n * SARIF 2.1.0 writer — port of strix report/sarif.py: rule ids normalized\n * CWE→CVE→id→slug, GitHub security-severity, STRIDE tags from the CWE map,\n * physical + synthetic (SECURITY.md anchor) locations with endpoint logical\n * locations, PR-suggestion fixes, deterministic partial fingerprints\n * (sha256), and the strix-namespaced properties (PoC script body never\n * exported). Key insertion order matches Python dict construction\n * byte-for-byte (golden-diff locked).\n * @module @gpzhang2001/sharpkit-reporting/sarif\n */\nimport type { VulnerabilityReport } from './state.ts';\nexport declare const SARIF_SCHEMA = \"https://json.schemastore.org/sarif-2.1.0.json\";\nexport declare const SARIF_VERSION = \"2.1.0\";\nexport declare const TOOL_NAME = \"sharpkit\";\nexport declare const TOOL_INFORMATION_URI = \"https://github.com/gpzhang2001/sharpkit\";\ntype Json = string | number | boolean | null | Json[] | {\n [key: string]: Json;\n};\n/** Stable rule id: CWE → CVE → finding id → slug → sharpkit-finding. */\nexport declare function ruleIdOf(report: VulnerabilityReport): string;\n/** Lowercase slug joined by dashes (sarif.py `_slugify`). */\nexport declare function slugify(value: string): string;\n/** STRIDE legs for a CWE, default legs when unmapped (every finding gets ≥1). */\nexport declare function strideLegsForCwe(cwe: unknown): readonly string[];\n/** First curated keyword in the title, else the first 5 alphanumeric words. */\nexport declare function classKeyword(title: string): string;\n/** SARIF level mapping. */\nexport declare function sarifLevel(severity: unknown): string;\n/** GitHub security-severity: \"%.1f\" CVSS else the label score. */\nexport declare function securitySeverity(report: VulnerabilityReport): string;\n/** Reject unsafe SARIF artifact URIs; normalize backslashes (sarif.py `_sarif_uri`). */\nexport declare function sarifUri(file: string): string | null;\n/** Deterministic per-finding fingerprint (sarif.py `_primary_fingerprint`). */\ndeclare function primaryFingerprint(ruleId: string, report: VulnerabilityReport, locations: readonly Json[], isSynthetic: boolean): string | null;\n/** File-independent class fingerprint (sarif.py `_class_fingerprint`). */\ndeclare function classFingerprint(ruleId: string, report: VulnerabilityReport): string | null;\n/** One coverage entry projected into SARIF (strix coverage.py shape). */\nexport interface SarifCoverageEntry {\n readonly risk_area: string;\n readonly surface: string;\n readonly outcome: string;\n readonly evidence?: unknown;\n readonly recorded_by?: unknown;\n}\n/** The coverage document subset the SARIF bridge consumes. */\nexport interface SarifCoverage {\n readonly entries: readonly SarifCoverageEntry[];\n readonly completeness?: {\n readonly complete?: boolean;\n readonly caveats?: readonly string[];\n } | undefined;\n}\nexport interface SarifOptions {\n readonly toolVersion: string;\n readonly coverage?: SarifCoverage | undefined;\n /** Exactly-one-repository provenance (strix passes None in our port for now). */\n readonly repositoryContext?: {\n readonly repositoryUri?: string;\n readonly repositoryFullName?: string;\n readonly commitSha?: string;\n readonly branch?: string;\n readonly ref?: string;\n } | undefined;\n}\n/**\n * Build the full SARIF document (top-level key order and run properties).\n * Coverage results and invocations join when the coverage tool lands (批 4).\n * @param reports - all stored reports.\n * @param options - tool version and optional repository provenance.\n */\nexport declare function buildSarif(reports: readonly VulnerabilityReport[], options: SarifOptions): {\n [key: string]: Json;\n};\n/**\n * Write findings.sarif (temp sibling + rename, trailing newline; sarif.py\n * `write_sarif_report`). Always emitted, even with zero findings, so a fresh\n * empty doc overwrites stale results.\n * @param runDir - the run directory.\n * @param reports - all stored reports.\n * @param options - tool version and provenance.\n */\nexport declare function writeSarif(runDir: string, reports: readonly VulnerabilityReport[], options: SarifOptions): Promise<void>;\n/** Reproduce a fingerprint for tests (exposed for golden diagnostics). */\nexport declare const internals: {\n primaryFingerprint: typeof primaryFingerprint;\n classFingerprint: typeof classFingerprint;\n};\nexport {};\n","/**\n * Vulnerability / dependency reporting tools — port of strix\n * tools/reporting/tool.py: create_vulnerability_report,\n * create_dependency_report, update_vulnerability_report, list_reports,\n * get_report. Validation parity (runtime-validated value sets, CVSS\n * computation, cross-class update guards), strix's exact response JSON\n * shapes, and the artifacts fan-out on every mutation (md/csv/json/sarif +\n * run.json last, atomic writes; SARIF always emitted even when empty).\n * The dedupe LLM judge is a Config callback (deterministic dependency fast\n * path runs regardless; judge failures default to not-duplicate).\n * @module @gpzhang2001/sharpkit-reporting\n */\nimport type { Context } from '@deepseek-ai/cordis';\nimport type Schema from '@deepseek-ai/schemastery';\nimport { type DedupeJudge } from './dedupe.ts';\nimport { ReportState } from './state.ts';\nimport { renderVulnerabilityMd } from './writers.ts';\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n pentestReporting: ReportingHandle;\n }\n}\nexport { renderVulnerabilityMd };\nexport { ReportState, cleanTitle, severityRank } from './state.ts';\nexport { calculateCvss, cvssBaseScore, cvssSeverity, validateCvssBreakdown, dependencySeverity, buildCvssVector } from './cvss.ts';\nexport { checkDuplicate, dependencyIdentity } from './dedupe.ts';\nexport { buildSarif, sarifUri, ruleIdOf, classKeyword } from './sarif.ts';\n/** Deployment-tunable configuration. */\nexport interface Config {\n /** Run-name slug basis (strix derives it from the target label). */\n readonly runName?: string;\n /** Runs root directory (sharpkit_runs; configurable back to strix_runs for legacy compat). */\n readonly runsRoot?: string;\n /** Sandbox image version stamped into SARIF (tool.driver.version). */\n readonly toolVersion?: string;\n /** Scan mode recorded into run.json. */\n readonly scanMode?: string;\n /** Targets recorded into run.json. */\n readonly targetsInfo?: readonly Record<string, unknown>[];\n /** Pluggable dedupe LLM judge (dynamic findings; failures = not duplicate). */\n readonly dedupeJudge?: DedupeJudge;\n /** File the CVSS-broad-CWE ban borrows from strix's guidance. */\n readonly strictCwe?: boolean;\n /** Coverage document source (the analysis package wires itself in). */\n readonly coverageSource?: CoverageSource;\n /** Auth mode recorded into run.json (strix codex.auth_mode; default \"none\"). */\n readonly authMode?: string;\n /** Scan instruction recorded into run.json (strix set_scan_config). */\n readonly instruction?: string;\n /** Diff scope recorded into run.json (diff-scope scans). */\n readonly diffScope?: string;\n /** Non-interactive flag recorded into run.json. */\n readonly nonInteractive?: boolean;\n /** Local source trees recorded into run.json. */\n readonly localSources?: readonly Record<string, unknown>[];\n /** Scope mode recorded into run.json (strix default \"auto\"). */\n readonly scopeMode?: string;\n /** Diff base ref recorded into run.json (diff-scope scans). */\n readonly diffBase?: string;\n /** MCP connection names recorded into run.json (strix record_mcp_connections). */\n readonly mcpConnections?: readonly string[];\n}\nexport declare const name = \"pentest-tool-reporting\";\nexport declare const inject: string[];\nexport declare const Config: Schema<Config>;\n/** Normalize + validate code_locations (tool.py `_normalize_code_locations`). */\nexport declare function normalizeCodeLocations(raw: unknown): {\n readonly locations?: Record<string, unknown>[];\n readonly errors: string[];\n};\n/** CVE normalization (tool.py `_extract_cve` + `_validate_cve`). */\nexport declare function normalizeCve(value: string | undefined): string | undefined;\n/** CWE normalization (tool.py `_extract_cwe`). */\nexport declare function normalizeCwe(value: string | undefined): string | undefined;\n/** Dependency metadata builder (tool.py `_build_dependency_metadata` order). */\nexport declare function buildDependencyMetadata(fields: {\n readonly packageName: string;\n readonly installedVersion: string;\n readonly advisoryCvss?: number | undefined;\n readonly packageEcosystem: string;\n readonly manifestPath: string;\n readonly fixedVersion?: string | undefined;\n readonly introducedBy?: string | undefined;\n readonly dependencyPath?: string | undefined;\n readonly reachability: string;\n readonly reachabilityEvidence?: string | undefined;\n readonly contextual?: {\n readonly breakdown: Record<string, string>;\n readonly score: number;\n readonly vector: string;\n readonly reasoning: string;\n } | undefined;\n}): Record<string, unknown>;\nexport interface ReportingHandle {\n readonly state: ReportState;\n readonly runDir: string;\n finishScan(sections: {\n readonly executiveSummary: string;\n readonly methodology: string;\n readonly technicalAnalysis: string;\n readonly recommendations: string;\n }, status?: string): Promise<void>;\n writeNow(): Promise<void>;\n readRaw(relative: string): Promise<string>;\n}\n/** Coverage document provider the analysis package supplies at wiring time. */\nexport interface CoverageSource {\n entries(): Array<Record<string, unknown>>;\n outcomeCounts(): Record<string, number>;\n}\nexport declare function apply(ctx: Context, config?: Config): ReportingHandle;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.ts","names":["Schema"],"sources":["../src/state.d.ts","../src/dedupe.d.ts","../src/writers.d.ts","../src/cvss.d.ts","../src/sarif.d.ts","../src/index.d.ts"],"sourcesContent":["/**\n * Report state — port of strix report/state.py: the per-scan report store\n * with sequential `vuln-NNNN` ids, strix's exact field insertion order,\n * title cleaning, update whitelist with dependent-field dropping, update\n * history, and hydration from a previous run dir. Key order matters: the\n * stored dicts are JSON-serialized byte-for-byte into vulnerabilities.json,\n * so fields are inserted in state.py `add_vulnerability_report` order.\n * @module @gpzhang2001/sharpkit-reporting/state\n */\n/** A stored vulnerability/dependency report dict (strix shape). */\nexport interface VulnerabilityReport {\n readonly id: string;\n title: string;\n severity: string;\n readonly timestamp: string;\n [key: string]: unknown;\n}\n/** Injectable clock (tests pass fixed times; default wall clock). */\nexport type Clock = () => Date;\n/** strix display timestamp format (state.py :349). */\nexport declare function formatTimestamp(date: Date): string;\n/** strix ISO instant format (start/end times). */\nexport declare function formatIso(date: Date): string;\n/** Control-char → space + whitespace collapse (state.py `_clean_title`). */\nexport declare function cleanTitle(title: string): string;\n/** strix severity order (tool.py `_SEVERITY_ORDER`). */\nexport declare const SEVERITY_ORDER: readonly [\"critical\", \"high\", \"medium\", \"low\", \"info\", \"none\"];\n/** Severity rank with unknown → last (writer parity). */\nexport declare function severityRank(severity: string): number;\n/** Updatable field whitelist (state.py `UPDATABLE_REPORT_FIELDS`). */\nexport declare const UPDATABLE_REPORT_FIELDS: Set<string>;\n/** Dependent fields dropped when their primary changes without replacement. */\nexport declare const DEPENDENT_REPORT_FIELDS: Readonly<Record<string, string>>;\n/** One update-history entry (state.py :467-487). */\nexport interface UpdateHistoryEntry {\n readonly timestamp: string;\n fields: string[];\n dropped_fields?: string[];\n reason?: string;\n agent_id?: string;\n agent_name?: string;\n previous_severity?: string;\n previous_cvss?: number;\n previous_confidence?: string;\n}\nexport interface AddReportInput {\n readonly title: string;\n readonly severity: string;\n readonly findingClass?: string | undefined;\n readonly dependencyMetadata?: Record<string, unknown> | undefined;\n readonly agentId?: string | undefined;\n readonly agentName?: string | undefined;\n /** All remaining report fields (already validated/normalized by the tool layer). */\n readonly fields: Record<string, unknown>;\n}\n/** The revision outcome: null = no-op (strix parity). */\nexport type UpdateOutcome = {\n readonly report: VulnerabilityReport;\n} | {\n readonly noop: true;\n};\n/**\n * One scan's report store. Not a process singleton — the suite runs one per\n * scan id inside the tool package's closure (strix's module-global maps to a\n * per-scan instance in dsh).\n */\nexport declare class ReportState {\n /** Reseated per resolved run dir (see reseatRunId); NOT per-process — the state is shared for multi-round continuation. */\n runId: string;\n runName: string | null;\n readonly startTime: string;\n endTime: string | null;\n status: string;\n finalScanResult: string | null;\n readonly vulnerabilityReports: VulnerabilityReport[];\n /** Ids already rendered to markdown (incremental writer input). */\n readonly savedVulnIds: Set<string>;\n updateHistoryAgent: {\n readonly agentId?: string;\n readonly agentName?: string;\n } | undefined;\n private readonly clock;\n constructor(options?: {\n readonly runId?: string;\n readonly runName?: string | null;\n readonly clock?: Clock;\n });\n /**\n * Fresh run id for a newly resolved run directory. The state object stays\n * process-shared so a second scan in the same host process CONTINUES the\n * report ledger (multi-round retest mode) — but each run dir must carry its\n * own id: before this, both rounds' run.json showed the same run_id because\n * the id was minted once per process (2026-09-22).\n */\n reseatRunId(): void;\n /** Allocate the next sequential id (state.py :343 — length-derived). */\n private nextId;\n /**\n * Add one report with strix's exact field construction order.\n * @param input - the validated/normalized create payload.\n */\n addVulnerabilityReport(input: AddReportInput): VulnerabilityReport;\n /**\n * Revise one report in place (state.py `update_vulnerability_report`):\n * whitelist + strip/lowercase, unchanged-skip, dependent-field dropping,\n * history append, and the MD invalidation marker.\n * @param reportId - the `vuln-NNNN` id.\n * @param changes - only the fields the model passed.\n * @param reason - the update_reason (truncated to 500).\n */\n updateVulnerabilityReport(reportId: string, changes: Record<string, unknown>, reason: string): UpdateOutcome;\n /**\n * Reload from a previous run's vulnerabilities.json so id allocation does\n * not collide (state.py `hydrate_from_run_dir`; raises on corrupt JSON).\n * @param reports - the parsed JSON array.\n */\n hydrate(reports: unknown): void;\n /** Mark the run complete (status transition + end time). */\n complete(exitStatus?: string): void;\n}\n","/**\n * Dedupe — port of strix report/dedupe.py decision flow: the deterministic\n * dependency identity fast path (CVE × package × ecosystem, distinct\n * manifests are separate findings, legacy word-bounded mention fallback) and\n * an injectable LLM judge for dynamic findings. Every judge failure defaults\n * to NOT duplicate (strix parity: dedupe failures never block a finding).\n * @module @gpzhang2001/sharpkit-reporting/dedupe\n */\nimport type { VulnerabilityReport } from './state.ts';\n/** Dependency identity fields (strix `_dependency_identity`). */\nexport interface DependencyIdentity {\n readonly cve: string;\n readonly packageName: string;\n readonly ecosystem: string;\n}\n/** The dedupe verdict shape (strix `DuplicateCheckResult`). */\nexport interface DuplicateVerdict {\n readonly isDuplicate: boolean;\n readonly duplicateId: string;\n readonly confidence: number;\n readonly reason: string;\n}\n/** Pluggable LLM judge (Config-injected; dsh-side LLM wiring lands with M4). */\nexport type DedupeJudge = (candidate: Record<string, unknown>, existing: readonly VulnerabilityReport[]) => Promise<DuplicateVerdict>;\n/** Extract the dependency identity from metadata (strix :162-177). */\nexport declare function dependencyIdentity(metadata: unknown): DependencyIdentity | null;\n/** Whether two manifest paths are both present AND different (strix `_distinct_manifest_paths`). */\nexport declare function distinctManifestPaths(a: unknown, b: unknown): boolean;\n/** Word-bounded regex mention check over prose fields (strix `_legacy_report_mentions_package`). */\nexport declare function legacyReportMentionsPackage(report: VulnerabilityReport, identity: DependencyIdentity): boolean;\n/**\n * The deterministic dependency fast path (strix `_check_dependency_duplicate`).\n * @param candidateIdentity - the candidate's identity.\n * @param candidateMetadata - the candidate's dependency metadata (manifest comparison).\n * @param existing - current reports.\n * @returns a verdict, or null to defer to the LLM judge.\n */\nexport declare function checkDependencyDuplicate(candidateIdentity: DependencyIdentity, candidateMetadata: Record<string, unknown> | undefined, existing: readonly VulnerabilityReport[]): DuplicateVerdict | null;\n/**\n * Entry point (strix `check_duplicate`): fast path for dependency\n * candidates, then the injected judge; every judge failure is NOT duplicate.\n * @param candidate - the candidate fields sent for comparison.\n * @param candidateMetadata - dependency metadata when present.\n * @param existing - current reports.\n * @param judge - the injected LLM judge (absent → not duplicate).\n */\nexport declare function checkDuplicate(candidate: Record<string, unknown>, candidateMetadata: Record<string, unknown> | undefined, existing: readonly VulnerabilityReport[], judge: DedupeJudge | undefined): Promise<DuplicateVerdict>;\n","/**\n * Artifact writers — port of strix report/writer.py: atomic writes\n * (temp-in-same-dir + rename, no fsync), run.json, vulnerabilities.csv\n * (formula-injection guard, \\r\\n terminators, uppercase severity,\n * severity-then-timestamp ordering), vulnerabilities.json (byte-identical\n * serialization), the vuln-NNNN.md renderer (exact section order), and the\n * executive report template.\n * @module @gpzhang2001/sharpkit-reporting/writers\n */\nimport { type VulnerabilityReport } from './state.ts';\n/** JSON.stringify with Python `json.dumps(ensure_ascii=False, indent=2)` parity. */\nexport declare function dumpsIndent(value: unknown): string;\n/**\n * Atomic text write (writer.py `atomic_write_text` :201-220): temp file in\n * the target's directory + rename; no fsync (strix parity).\n * @param path - target file path.\n * @param payload - exact bytes to write.\n */\nexport declare function atomicWriteText(path: string, payload: string): Promise<void>;\n/**\n * Formula-injection guard (writer.py `csv_safe` :35-53): prefix `'` when the\n * rendered cell starts with `= + - @ \\t \\r`.\n * @param value - the cell value.\n */\nexport declare function csvSafe(value: string): string;\n/**\n * Render vulnerabilities.csv: header + rows sorted by (severity rank,\n * timestamp), uppercase severity, \\r\\n terminators.\n * @param reports - the stored reports.\n */\nexport declare function renderVulnerabilitiesCsv(reports: readonly VulnerabilityReport[]): string;\n/** Update history section (writer.py `render_update_history` :364-396). */\nexport declare function renderUpdateHistory(report: Record<string, unknown>): string[];\n/**\n * Render one vulnerability markdown document (writer.py\n * `render_vulnerability_md` :223-361 section order).\n * @param report - the stored report dict.\n */\nexport declare function renderVulnerabilityMd(report: VulnerabilityReport): string;\n/**\n * Write the per-run artifacts handled outside SARIF (writer.py parity):\n * vulnerabilities/*.md (incremental), vulnerabilities.csv,\n * vulnerabilities.json.\n * @param runDir - the run directory.\n * @param reports - all stored reports.\n * @param savedIds - ids whose MD is already on disk (updated ids must be re-rendered by the caller removing them).\n */\nexport declare function writeVulnerabilities(runDir: string, reports: readonly VulnerabilityReport[], savedIds: ReadonlySet<string>): Promise<void>;\n/**\n * Write the assembled coverage document (coverage.py `write_coverage`).\n * @param runDir - the run directory.\n * @param document - the assembled coverage document.\n */\nexport declare function writeCoverage(runDir: string, document: Record<string, unknown>): Promise<void>;\n/**\n * Write run.json last (state.py ordering).\n * @param runDir - the run directory.\n * @param runRecord - the run record dict.\n */\nexport declare function writeRunRecord(runDir: string, runRecord: Record<string, unknown>): Promise<void>;\n/**\n * Write the executive report (plain truncate-write, writer.py :139-145).\n * @param runDir - the run directory.\n * @param finalScanResult - the composed final report body.\n * @param generatedAt - display timestamp.\n */\nexport declare function writeExecutiveReport(runDir: string, finalScanResult: string, generatedAt: string): Promise<void>;\n/** Severity list used by callers that need the canonical order. */\nexport declare const severityOrder: readonly [\"critical\", \"high\", \"medium\", \"low\", \"info\", \"none\"];\n","/**\n * CVSS v3.1 base-score math, ported from strix's usage of the `cvss`\n * package (tool.py `_calculate_cvss` :134-153): build the vector string,\n * compute the base score and qualitative severity. The score uses the\n * CVSS 3.1 spec formula with the spec's Roundup1 (ceil to one decimal with\n * the IEEE-754 guard), and a \"none\" base severity is remapped to \"info\"\n * (strix parity).\n * @module @gpzhang2001/sharpkit-reporting/cvss\n */\n/** The eight CVSS metrics with their allowed values (strix `_CVSS_VALID`). */\nexport declare const CVSS_VALID: {\n readonly attack_vector: readonly [\"N\", \"A\", \"L\", \"P\"];\n readonly attack_complexity: readonly [\"L\", \"H\"];\n readonly privileges_required: readonly [\"N\", \"L\", \"H\"];\n readonly user_interaction: readonly [\"N\", \"R\"];\n readonly scope: readonly [\"U\", \"C\"];\n readonly confidentiality: readonly [\"N\", \"L\", \"H\"];\n readonly integrity: readonly [\"N\", \"L\", \"H\"];\n readonly availability: readonly [\"N\", \"L\", \"H\"];\n};\nexport type CvssMetricName = keyof typeof CVSS_VALID;\n/**\n * Validate a breakdown against the allowed metric/value sets.\n * @param breakdown - the model-supplied 8-metric dict.\n * @returns validation errors, empty when valid.\n */\nexport declare function validateCvssBreakdown(breakdown: Record<string, unknown>): string[];\n/**\n * Build the vector string (strix format: CVSS:3.1/AV:../AC:../PR:../UI:../S:../C:../I:../A:..).\n * @param breakdown - a validated breakdown.\n */\nexport declare function buildCvssVector(breakdown: Readonly<Record<CvssMetricName, string>>): string;\n/**\n * Compute the CVSS v3.1 base score for a breakdown (the `cvss` package's\n * `compute_base_score`: Scope U ISC = 6.42×ISCBase; Scope C ISC = the 7.52\n * formula; Scope C multiplies the total by 1.08 before the cap).\n * @param breakdown - a validated breakdown.\n * @returns the rounded base score (0.0 when impact is non-positive).\n */\nexport declare function cvssBaseScore(breakdown: Readonly<Record<CvssMetricName, string>>): number;\n/**\n * Qualitative severity banding (the `cvss` package's rating bands).\n * @param score - the base score.\n */\nexport declare function cvssSeverity(score: number): 'none' | 'low' | 'medium' | 'high' | 'critical';\n/**\n * Full computation: vector + score + severity with the \"none\"→\"info\" remap\n * (strix `_calculate_cvss` return contract).\n * @param breakdown - a validated breakdown.\n */\nexport declare function calculateCvss(breakdown: Readonly<Record<CvssMetricName, string>>): {\n readonly vector: string;\n readonly score: number;\n readonly severity: string;\n};\n/**\n * Dependency severity banding from an advisory score (strix\n * `_DEP_SEVERITY_FROM_CVSS` :1363-1378, top band inclusive at 10.0).\n * @param score - the advisory base score.\n */\nexport declare function dependencySeverity(score: number): string;\n","/**\n * SARIF 2.1.0 writer — port of strix report/sarif.py: rule ids normalized\n * CWE→CVE→id→slug, GitHub security-severity, STRIDE tags from the CWE map,\n * physical + synthetic (SECURITY.md anchor) locations with endpoint logical\n * locations, PR-suggestion fixes, deterministic partial fingerprints\n * (sha256), and the strix-namespaced properties (PoC script body never\n * exported). Key insertion order matches Python dict construction\n * byte-for-byte (golden-diff locked).\n * @module @gpzhang2001/sharpkit-reporting/sarif\n */\nimport type { VulnerabilityReport } from './state.ts';\nexport declare const SARIF_SCHEMA = \"https://json.schemastore.org/sarif-2.1.0.json\";\nexport declare const SARIF_VERSION = \"2.1.0\";\nexport declare const TOOL_NAME = \"sharpkit\";\nexport declare const TOOL_INFORMATION_URI = \"https://github.com/gpzhang2001/sharpkit\";\ntype Json = string | number | boolean | null | Json[] | {\n [key: string]: Json;\n};\n/** Stable rule id: CWE → CVE → finding id → slug → sharpkit-finding. */\nexport declare function ruleIdOf(report: VulnerabilityReport): string;\n/** Lowercase slug joined by dashes (sarif.py `_slugify`). */\nexport declare function slugify(value: string): string;\n/** STRIDE legs for a CWE, default legs when unmapped (every finding gets ≥1). */\nexport declare function strideLegsForCwe(cwe: unknown): readonly string[];\n/** First curated keyword in the title, else the first 5 alphanumeric words. */\nexport declare function classKeyword(title: string): string;\n/** SARIF level mapping. */\nexport declare function sarifLevel(severity: unknown): string;\n/** GitHub security-severity: \"%.1f\" CVSS else the label score. */\nexport declare function securitySeverity(report: VulnerabilityReport): string;\n/** Reject unsafe SARIF artifact URIs; normalize backslashes (sarif.py `_sarif_uri`). */\nexport declare function sarifUri(file: string): string | null;\n/** Deterministic per-finding fingerprint (sarif.py `_primary_fingerprint`). */\ndeclare function primaryFingerprint(ruleId: string, report: VulnerabilityReport, locations: readonly Json[], isSynthetic: boolean): string | null;\n/** File-independent class fingerprint (sarif.py `_class_fingerprint`). */\ndeclare function classFingerprint(ruleId: string, report: VulnerabilityReport): string | null;\n/** One coverage entry projected into SARIF (strix coverage.py shape). */\nexport interface SarifCoverageEntry {\n readonly risk_area: string;\n readonly surface: string;\n readonly outcome: string;\n readonly evidence?: unknown;\n readonly recorded_by?: unknown;\n}\n/** The coverage document subset the SARIF bridge consumes. */\nexport interface SarifCoverage {\n readonly entries: readonly SarifCoverageEntry[];\n readonly completeness?: {\n readonly complete?: boolean;\n readonly caveats?: readonly string[];\n } | undefined;\n}\nexport interface SarifOptions {\n readonly toolVersion: string;\n readonly coverage?: SarifCoverage | undefined;\n /** Exactly-one-repository provenance (strix passes None in our port for now). */\n readonly repositoryContext?: {\n readonly repositoryUri?: string;\n readonly repositoryFullName?: string;\n readonly commitSha?: string;\n readonly branch?: string;\n readonly ref?: string;\n } | undefined;\n}\n/**\n * Build the full SARIF document (top-level key order and run properties).\n * Coverage results and invocations join when the coverage tool lands (批 4).\n * @param reports - all stored reports.\n * @param options - tool version and optional repository provenance.\n */\nexport declare function buildSarif(reports: readonly VulnerabilityReport[], options: SarifOptions): {\n [key: string]: Json;\n};\n/**\n * Write findings.sarif (temp sibling + rename, trailing newline; sarif.py\n * `write_sarif_report`). Always emitted, even with zero findings, so a fresh\n * empty doc overwrites stale results.\n * @param runDir - the run directory.\n * @param reports - all stored reports.\n * @param options - tool version and provenance.\n */\nexport declare function writeSarif(runDir: string, reports: readonly VulnerabilityReport[], options: SarifOptions): Promise<void>;\n/** Reproduce a fingerprint for tests (exposed for golden diagnostics). */\nexport declare const internals: {\n primaryFingerprint: typeof primaryFingerprint;\n classFingerprint: typeof classFingerprint;\n};\nexport {};\n","/**\n * Vulnerability / dependency reporting tools — port of strix\n * tools/reporting/tool.py: create_vulnerability_report,\n * create_dependency_report, update_vulnerability_report, list_reports,\n * get_report. Validation parity (runtime-validated value sets, CVSS\n * computation, cross-class update guards), strix's exact response JSON\n * shapes, and the artifacts fan-out on every mutation (md/csv/json/sarif +\n * run.json last, atomic writes; SARIF always emitted even when empty).\n * The dedupe LLM judge is a Config callback (deterministic dependency fast\n * path runs regardless; judge failures default to not-duplicate).\n * @module @gpzhang2001/sharpkit-reporting\n */\nimport type { Context } from '@deepseek-ai/cordis';\nimport type Schema from '@deepseek-ai/schemastery';\nimport { type DedupeJudge } from './dedupe.ts';\nimport { ReportState } from './state.ts';\nimport { renderVulnerabilityMd } from './writers.ts';\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n pentestReporting: ReportingHandle;\n }\n}\nexport { renderVulnerabilityMd };\nexport { ReportState, cleanTitle, severityRank } from './state.ts';\nexport { calculateCvss, cvssBaseScore, cvssSeverity, validateCvssBreakdown, dependencySeverity, buildCvssVector } from './cvss.ts';\nexport { checkDuplicate, dependencyIdentity } from './dedupe.ts';\nexport { buildSarif, sarifUri, ruleIdOf, classKeyword } from './sarif.ts';\n/** Deployment-tunable configuration. */\nexport interface Config {\n /** Run-name slug basis (strix derives it from the target label). */\n readonly runName?: string;\n /** Runs root directory (sharpkit_runs; configurable back to strix_runs for legacy compat). */\n readonly runsRoot?: string;\n /** Sandbox image version stamped into SARIF (tool.driver.version). */\n readonly toolVersion?: string;\n /** Scan mode recorded into run.json. */\n readonly scanMode?: string;\n /** Targets recorded into run.json. */\n readonly targetsInfo?: readonly Record<string, unknown>[];\n /** Pluggable dedupe LLM judge (dynamic findings; failures = not duplicate). */\n readonly dedupeJudge?: DedupeJudge;\n /** File the CVSS-broad-CWE ban borrows from strix's guidance. */\n readonly strictCwe?: boolean;\n /** Coverage document source (the analysis package wires itself in). */\n readonly coverageSource?: CoverageSource;\n /** Auth mode recorded into run.json (strix codex.auth_mode; default \"none\"). */\n readonly authMode?: string;\n /** Scan instruction recorded into run.json (strix set_scan_config). */\n readonly instruction?: string;\n /** Diff scope recorded into run.json (diff-scope scans). */\n readonly diffScope?: string;\n /** Non-interactive flag recorded into run.json. */\n readonly nonInteractive?: boolean;\n /** Local source trees recorded into run.json. */\n readonly localSources?: readonly Record<string, unknown>[];\n /** Scope mode recorded into run.json (strix default \"auto\"). */\n readonly scopeMode?: string;\n /** Diff base ref recorded into run.json (diff-scope scans). */\n readonly diffBase?: string;\n /** MCP connection names recorded into run.json (strix record_mcp_connections). */\n readonly mcpConnections?: readonly string[];\n}\nexport declare const name = \"pentest-tool-reporting\";\nexport declare const inject: string[];\nexport declare const Config: Schema<Config>;\n/** Normalize + validate code_locations (tool.py `_normalize_code_locations`). */\nexport declare function normalizeCodeLocations(raw: unknown): {\n readonly locations?: Record<string, unknown>[];\n readonly errors: string[];\n};\n/** CVE normalization (tool.py `_extract_cve` + `_validate_cve`). */\nexport declare function normalizeCve(value: string | undefined): string | undefined;\n/** CWE normalization (tool.py `_extract_cwe`). */\nexport declare function normalizeCwe(value: string | undefined): string | undefined;\n/** Dependency metadata builder (tool.py `_build_dependency_metadata` order). */\nexport declare function buildDependencyMetadata(fields: {\n readonly packageName: string;\n readonly installedVersion: string;\n readonly advisoryCvss?: number | undefined;\n readonly packageEcosystem: string;\n readonly manifestPath: string;\n readonly fixedVersion?: string | undefined;\n readonly introducedBy?: string | undefined;\n readonly dependencyPath?: string | undefined;\n readonly reachability: string;\n readonly reachabilityEvidence?: string | undefined;\n readonly contextual?: {\n readonly breakdown: Record<string, string>;\n readonly score: number;\n readonly vector: string;\n readonly reasoning: string;\n } | undefined;\n}): Record<string, unknown>;\nexport interface ReportingHandle {\n readonly state: ReportState;\n /** Directory of the most recently resolved run (sync view; resolve via {@link runDirFor} for accuracy). */\n readonly runDir: string;\n /** Resolve (memoize per scan) this session's run directory — see apply() for the rules. */\n runDirFor(session: unknown): Promise<string>;\n finishScan(sections: {\n readonly executiveSummary: string;\n readonly methodology: string;\n readonly technicalAnalysis: string;\n readonly recommendations: string;\n }, status?: string, extras?: Readonly<Record<string, unknown>>): Promise<void>;\n writeNow(): Promise<void>;\n readRaw(relative: string): Promise<string>;\n}\n/** Coverage document provider the analysis package supplies at wiring time. */\nexport interface CoverageSource {\n entries(): Array<Record<string, unknown>>;\n outcomeCounts(): Record<string, number>;\n}\nexport declare function apply(ctx: Context, config?: Config): ReportingHandle;\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
- import { dirname, join, resolve } from "node:path";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
4
  import z from "@deepseek-ai/schemastery";
4
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
6
  import { createHash } from "node:crypto";
@@ -410,6 +411,7 @@ const DEPENDENT_REPORT_FIELDS = {
410
411
  * per-scan instance in dsh).
411
412
  */
412
413
  var ReportState = class {
414
+ /** Reseated per resolved run dir (see reseatRunId); NOT per-process — the state is shared for multi-round continuation. */
413
415
  runId;
414
416
  runName;
415
417
  startTime;
@@ -427,6 +429,16 @@ var ReportState = class {
427
429
  this.runName = options.runName ?? null;
428
430
  this.startTime = formatIso(this.clock());
429
431
  }
432
+ /**
433
+ * Fresh run id for a newly resolved run directory. The state object stays
434
+ * process-shared so a second scan in the same host process CONTINUES the
435
+ * report ledger (multi-round retest mode) — but each run dir must carry its
436
+ * own id: before this, both rounds' run.json showed the same run_id because
437
+ * the id was minted once per process (2026-09-22).
438
+ */
439
+ reseatRunId() {
440
+ this.runId = `run-${Math.random().toString(16).slice(2, 10)}`;
441
+ }
430
442
  /** Allocate the next sequential id (state.py :343 — length-derived). */
431
443
  nextId() {
432
444
  return `vuln-${String(this.vulnerabilityReports.length + 1).padStart(4, "0")}`;
@@ -1673,39 +1685,154 @@ const DYNAMIC_ONLY_UPDATE_FIELDS = /* @__PURE__ */ new Set([
1673
1685
  ]);
1674
1686
  function apply(ctx, config = {}) {
1675
1687
  const runsRoot = config.runsRoot ?? "sharpkit_runs";
1676
- const runName = config.runName ?? `pentest-${Math.random().toString(16).slice(2, 6)}`;
1677
- const runDir = resolve(join(resolve(runsRoot), runName));
1678
- const state = new ReportState({ runName });
1688
+ const fallbackRunName = config.runName ?? `pentest-${Math.random().toString(16).slice(2, 6)}`;
1689
+ const state = new ReportState({ runName: fallbackRunName });
1679
1690
  const toolVersion = config.toolVersion ?? "0.1.0";
1680
1691
  /** scan_results block, set by finishScan and appended to run.json. */
1681
1692
  let scanResults;
1682
- const usageLedger = {
1683
- requests: 0,
1684
- inputTokens: 0,
1685
- outputTokens: 0,
1686
- totalTokens: 0
1693
+ const scanFactsOf = (session) => {
1694
+ const header = session?.header;
1695
+ const id = typeof header?.id === "string" ? header.id : "";
1696
+ const parent = typeof header?.parentSession === "string" ? header.parentSession : "";
1697
+ const cwd = typeof header?.cwd === "string" && header.cwd !== "" ? header.cwd : "";
1698
+ return {
1699
+ id,
1700
+ key: parent !== "" ? parent : id,
1701
+ cwd
1702
+ };
1703
+ };
1704
+ const sessionOf = (exec) => exec?.agent?.session;
1705
+ const AGENTLESS = ":agentless";
1706
+ /** Per-scan memo: resolved run dir by scan key (root session id). */
1707
+ const resolvedRunDirs = /* @__PURE__ */ new Map();
1708
+ /** The session noted by the most recent tool execute (one scan per host at a time). */
1709
+ let activeSession;
1710
+ /** sessionId → scan key: lets the usage ledger attribute assistant messages to the owning scan. */
1711
+ const scanKeyOfSession = /* @__PURE__ */ new Map();
1712
+ const noteScanKey = (session) => {
1713
+ const facts = scanFactsOf(session);
1714
+ if (facts.id === "") return;
1715
+ const previous = scanKeyOfSession.get(facts.id);
1716
+ scanKeyOfSession.set(facts.id, facts.key);
1717
+ if (previous !== facts.key && facts.key !== facts.id) {
1718
+ const orphan = usageByScan.get(facts.id);
1719
+ if (orphan !== void 0) {
1720
+ const root = usageAccountOf(facts.key);
1721
+ root.requests += orphan.requests;
1722
+ root.inputTokens += orphan.inputTokens;
1723
+ root.outputTokens += orphan.outputTokens;
1724
+ root.totalTokens += orphan.totalTokens;
1725
+ usageByScan.delete(facts.id);
1726
+ }
1727
+ }
1728
+ };
1729
+ const noteExec = (rawExec) => {
1730
+ const session = sessionOf(rawExec);
1731
+ if (session !== void 0) {
1732
+ activeSession = session;
1733
+ noteScanKey(session);
1734
+ }
1735
+ };
1736
+ /** Occupancy facts of an existing run dir (best-effort read of its run.json). */
1737
+ const runInfoOf = async (dir) => {
1738
+ try {
1739
+ const raw = JSON.parse(await readFile(join(dir, "run.json"), "utf8"));
1740
+ return {
1741
+ completed: raw["status"] === "completed" || raw["scan_results"]?.["scan_completed"] === true,
1742
+ scanKey: typeof raw["session_id"] === "string" ? raw["session_id"] : null
1743
+ };
1744
+ } catch {
1745
+ return {
1746
+ completed: false,
1747
+ scanKey: null
1748
+ };
1749
+ }
1687
1750
  };
1688
- ctx.on("session/event", (_session, event) => {
1751
+ /**
1752
+ * Resolve this scan's run directory, memoized per scan key:
1753
+ * - an absolute `runsRoot` pins the base (tests, explicit configs — unchanged);
1754
+ * - the default relative root resolves against the session header cwd,
1755
+ * falling back to process.cwd() for agentless calls;
1756
+ * - the name is the configured `runName`, else `pentest-<scan key short>`
1757
+ * (unique per scan, stable across restarts);
1758
+ * - a fresh scan NEVER overwrites a completed or foreign run: the name gains
1759
+ * a -2/-3… suffix; the same scan resuming its own incomplete run reuses
1760
+ * its directory (crash resume).
1761
+ */
1762
+ const runDirFor = async (session) => {
1763
+ if (session !== void 0) {
1764
+ activeSession = session;
1765
+ noteScanKey(session);
1766
+ }
1767
+ const facts = scanFactsOf(session);
1768
+ const key = facts.key !== "" ? facts.key : AGENTLESS;
1769
+ const memo = resolvedRunDirs.get(key);
1770
+ if (memo !== void 0) return memo;
1771
+ const base = isAbsolute(runsRoot) ? resolve(runsRoot) : resolve(facts.cwd !== "" ? facts.cwd : process.cwd(), runsRoot);
1772
+ const nameBase = config.runName ?? (facts.key !== "" ? `pentest-${facts.key.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8)}` : fallbackRunName);
1773
+ let name = nameBase;
1774
+ let dir = resolve(join(base, name));
1775
+ for (let suffix = 2; existsSync(dir); suffix++) {
1776
+ const info = await runInfoOf(dir);
1777
+ if (!info.completed && info.scanKey === key && key !== AGENTLESS) break;
1778
+ if (suffix === 2) ctx.logger.warn(`pentest-reporting: run dir '${name}' already holds ${info.completed ? "a completed run" : "another run"}; the new run goes to '${nameBase}-<n>'`);
1779
+ name = `${nameBase}-${suffix}`;
1780
+ dir = resolve(join(base, name));
1781
+ }
1782
+ resolvedRunDirs.set(key, dir);
1783
+ if (state.runName !== name) state.runName = name;
1784
+ state.reseatRunId();
1785
+ return dir;
1786
+ };
1787
+ /** Resolve the directory for the last-noted session (tools set it at execute start). */
1788
+ const currentRunDir = () => runDirFor(activeSession);
1789
+ const usageByScan = /* @__PURE__ */ new Map();
1790
+ const usageAccountOf = (key) => {
1791
+ let account = usageByScan.get(key);
1792
+ if (account === void 0) {
1793
+ account = {
1794
+ requests: 0,
1795
+ inputTokens: 0,
1796
+ outputTokens: 0,
1797
+ totalTokens: 0
1798
+ };
1799
+ usageByScan.set(key, account);
1800
+ }
1801
+ return account;
1802
+ };
1803
+ ctx.on("session/event", (session, event) => {
1689
1804
  const record = event;
1690
1805
  if (record.type !== "assistant/message") return;
1691
1806
  const usage = record.data?.usage;
1692
1807
  if (usage === void 0 || usage === null) return;
1693
- usageLedger.requests += 1;
1694
- usageLedger.inputTokens += usage.inputTokens ?? 0;
1695
- usageLedger.outputTokens += usage.outputTokens ?? 0;
1696
- usageLedger.totalTokens += usage.totalTokens ?? (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
1697
- });
1698
- const llmUsageRecord = () => ({
1699
- requests: usageLedger.requests,
1700
- input_tokens: usageLedger.inputTokens,
1701
- output_tokens: usageLedger.outputTokens,
1702
- total_tokens: usageLedger.totalTokens,
1703
- cost: null,
1704
- agents: []
1808
+ const sessionId = typeof session?.id === "string" ? session.id : "";
1809
+ if (sessionId === "") return;
1810
+ const account = usageAccountOf(scanKeyOfSession.get(sessionId) ?? sessionId);
1811
+ account.requests += 1;
1812
+ account.inputTokens += usage.inputTokens ?? 0;
1813
+ account.outputTokens += usage.outputTokens ?? 0;
1814
+ account.totalTokens += usage.totalTokens ?? (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0);
1705
1815
  });
1816
+ const llmUsageRecord = () => {
1817
+ const facts = activeSession !== void 0 ? scanFactsOf(activeSession) : {
1818
+ id: "",
1819
+ key: "",
1820
+ cwd: ""
1821
+ };
1822
+ const account = usageAccountOf(facts.key !== "" ? facts.key : AGENTLESS);
1823
+ return {
1824
+ requests: account.requests,
1825
+ input_tokens: account.inputTokens,
1826
+ output_tokens: account.outputTokens,
1827
+ total_tokens: account.totalTokens,
1828
+ cost: null,
1829
+ agents: []
1830
+ };
1831
+ };
1706
1832
  const ensureRunDir = async () => {
1707
- await mkdir(runDir, { recursive: true });
1708
- return runDir;
1833
+ const dir = await currentRunDir();
1834
+ await mkdir(dir, { recursive: true });
1835
+ return dir;
1709
1836
  };
1710
1837
  /** Resolve the coverage source: Config hook first, else the analysis package's service. */
1711
1838
  const coverageSource = () => {
@@ -1789,6 +1916,10 @@ function apply(ctx, config = {}) {
1789
1916
  const runRecord = {
1790
1917
  run_id: state.runId,
1791
1918
  run_name: state.runName,
1919
+ session_id: (() => {
1920
+ const key = scanFactsOf(activeSession).key;
1921
+ return key !== "" ? key : null;
1922
+ })(),
1792
1923
  start_time: state.startTime,
1793
1924
  end_time: state.endTime,
1794
1925
  status: state.status,
@@ -1840,7 +1971,7 @@ function apply(ctx, config = {}) {
1840
1971
  };
1841
1972
  ctx.tools.register(defineTool({
1842
1973
  name: "create_vulnerability_report",
1843
- description: "File a vulnerability report — one report per fully-verified finding, with a working PoC. Severity and CVSS are computed from the 8-metric cvss_breakdown you provide. On a duplicate verdict, revise the existing report via update_vulnerability_report instead of retrying. Never for known-CVE dependency findings — use create_dependency_report.",
1974
+ description: "File a vulnerability report — one report per fully-verified finding, with a working PoC. Severity and CVSS are computed from the 8-metric cvss_breakdown you provide. Validation contract (checked before filing, get these right the first time): cvss_breakdown must carry all 8 CVSS v3.1 metrics (attack_vector, attack_complexity, privileges_required, user_interaction, scope, confidentiality, integrity, availability) with their exact enum values; confidence != high requires confidence_rationale; any code_locations entry with fix_after requires fix_verification; cve must match CVE-YYYY-NNNNN and cwe must be a specific CWE-NNN child. On a duplicate verdict, revise the existing report via update_vulnerability_report instead of retrying. Never for known-CVE dependency findings — use create_dependency_report.",
1844
1975
  parameters: {
1845
1976
  title: {
1846
1977
  type: "string",
@@ -2002,6 +2133,7 @@ function apply(ctx, config = {}) {
2002
2133
  presentationMeta: (args, value) => findingPresentationMeta(args, value)
2003
2134
  },
2004
2135
  execute: (async (rawArgs, rawExec) => {
2136
+ noteExec(rawExec);
2005
2137
  const args = rawArgs;
2006
2138
  const errors = [];
2007
2139
  const breakdown = args.cvss_breakdown;
@@ -2188,6 +2320,7 @@ function apply(ctx, config = {}) {
2188
2320
  presentationMeta: (args, value) => findingPresentationMeta(args, value)
2189
2321
  },
2190
2322
  execute: (async (rawArgs, rawExec) => {
2323
+ noteExec(rawExec);
2191
2324
  const args = rawArgs;
2192
2325
  const reportId = cleanOptional(args.report_id);
2193
2326
  const reason = cleanOptional(args.update_reason);
@@ -2275,7 +2408,7 @@ function apply(ctx, config = {}) {
2275
2408
  }));
2276
2409
  ctx.tools.register(defineTool({
2277
2410
  name: "create_dependency_report",
2278
- description: "File a known-CVE dependency (SCA) finding — one report per CVE x package. For vulnerable third-party package versions pinned in a lockfile/manifest/SBOM; no live PoC needed. Severity comes from the contextual_cvss_breakdown when provided, else the advisory score. Never for dynamically-proven vulnerabilities — use create_vulnerability_report.",
2411
+ description: "File a known-CVE dependency (SCA) finding — one report per CVE x package. For vulnerable third-party package versions pinned in a lockfile/manifest/SBOM; no live PoC needed. Severity comes from the contextual_cvss_breakdown when provided, else the advisory score. Validation contract: besides the marked-required fields, manifest_path, reachability_evidence, contextual_cvss_breakdown AND contextual_cvss_reasoning are all required and must be non-empty (the '(required)' hints in their descriptions are enforced at execution time). Never for dynamically-proven vulnerabilities — use create_vulnerability_report.",
2279
2412
  parameters: {
2280
2413
  title: {
2281
2414
  type: "string",
@@ -2412,7 +2545,8 @@ function apply(ctx, config = {}) {
2412
2545
  duplicate_of: { type: "string" },
2413
2546
  confidence: { type: "number" },
2414
2547
  reason: { type: "string" },
2415
- warning: { type: "string" }
2548
+ warning: { type: "string" },
2549
+ cvss_score: { type: "number" }
2416
2550
  },
2417
2551
  additionalProperties: false
2418
2552
  },
@@ -2430,6 +2564,7 @@ function apply(ctx, config = {}) {
2430
2564
  presentationMeta: (args, value) => findingPresentationMeta(args, value)
2431
2565
  },
2432
2566
  execute: (async (rawArgs, _rawExec) => {
2567
+ noteExec(_rawExec);
2433
2568
  const args = rawArgs;
2434
2569
  const errors = [];
2435
2570
  const requireText = (value, name) => {
@@ -2596,6 +2731,7 @@ function apply(ctx, config = {}) {
2596
2731
  }
2597
2732
  },
2598
2733
  execute: (async (rawArgs, rawExec) => {
2734
+ noteExec(rawExec);
2599
2735
  const args = rawArgs;
2600
2736
  const severityFilter = cleanOptional(args.severity)?.toLowerCase();
2601
2737
  const classFilter = cleanOptional(args.finding_class)?.toLowerCase();
@@ -2685,6 +2821,7 @@ function apply(ctx, config = {}) {
2685
2821
  }
2686
2822
  },
2687
2823
  execute: (async (rawArgs, rawExec) => {
2824
+ noteExec(rawExec);
2688
2825
  const reportId = cleanOptional(rawArgs.report_id);
2689
2826
  if (reportId === void 0) return {
2690
2827
  success: false,
@@ -2758,9 +2895,22 @@ function apply(ctx, config = {}) {
2758
2895
  ].join("\n");
2759
2896
  const handle = {
2760
2897
  state,
2761
- runDir,
2898
+ /**
2899
+ * Sync view of the run directory: the resolved dir of the last-noted
2900
+ * session when one exists, else the unguarded default (no occupancy
2901
+ * check). Accuracy for a fresh session comes from runDirFor().
2902
+ */
2903
+ get runDir() {
2904
+ const facts = scanFactsOf(activeSession);
2905
+ const key = facts.key !== "" ? facts.key : ":agentless";
2906
+ const memo = resolvedRunDirs.get(key);
2907
+ if (memo !== void 0) return memo;
2908
+ const base = isAbsolute(runsRoot) ? resolve(runsRoot) : resolve(facts.cwd !== "" ? facts.cwd : process.cwd(), runsRoot);
2909
+ return resolve(join(base, config.runName ?? (facts.key !== "" ? `pentest-${facts.key.replace(/[^a-zA-Z0-9]/g, "").slice(0, 8)}` : fallbackRunName)));
2910
+ },
2911
+ runDirFor: (session) => runDirFor(session),
2762
2912
  /** Mark the scan complete and write the final report + artifacts. */
2763
- async finishScan(sections, status = "completed") {
2913
+ async finishScan(sections, status = "completed", extras) {
2764
2914
  state.finalScanResult = composeFinalReport(sections);
2765
2915
  state.complete(status);
2766
2916
  scanResults = {
@@ -2769,7 +2919,8 @@ function apply(ctx, config = {}) {
2769
2919
  methodology: sections.methodology,
2770
2920
  technical_analysis: sections.technicalAnalysis,
2771
2921
  recommendations: sections.recommendations,
2772
- success: status === "completed"
2922
+ success: status === "completed",
2923
+ ...extras
2773
2924
  };
2774
2925
  await writeExecutiveReport(await ensureRunDir(), state.finalScanResult, formatTimestamp(/* @__PURE__ */ new Date()));
2775
2926
  await saveArtifacts();
@@ -2778,7 +2929,7 @@ function apply(ctx, config = {}) {
2778
2929
  async writeNow() {
2779
2930
  await saveArtifacts();
2780
2931
  },
2781
- readRaw: async (relative) => readFile(join(runDir, relative), "utf8")
2932
+ readRaw: async (relative) => readFile(join(await currentRunDir(), relative), "utf8")
2782
2933
  };
2783
2934
  ctx.provide("pentestReporting", handle);
2784
2935
  return handle;