mjolnir-qa 2.1.0 → 3.0.0
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/CHANGELOG.md +54 -0
- package/dist/cli.d.mts +15 -3
- package/dist/cli.mjs +636 -390
- package/dist/mcp/stdio.mjs +436 -58
- package/dist/{scan-pipeline-MQGUSey3.mjs → scan-pipeline-CAH9_Qgh.mjs} +421 -51
- package/dist/scan-pipeline-CRe7-MQn.mjs +2 -0
- package/package.json +12 -5
- package/dist/scan-pipeline-C_Rs4u8N.mjs +0 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
3
|
-
import { accessSync, appendFileSync, chmodSync,
|
|
2
|
+
import { $ as correlateFindings, A as plainContext, At as parseJsonFile, B as shouldUseAscii, Bt as isValidCategory, C as runForensics, Ct as globToRegExp, D as nextStep, Dt as loadConfig, E as buildFooter, Et as isSuppressionActive, F as padTo, Ft as QA_IMPACT_LABELS, G as EVIDENCE, H as deriveScoreState, I as palette, It as RULE_CATEGORIES, J as atomicTempPath, K as TINT, L as sanitizeData, Lt as SEVERITY_ORDER$1, M as severityIcon, Mt as buildEvidenceGraph, N as box, Nt as MEASURED_FP, O as okIcon, Ot as readFileBounded, P as measure, Pt as DEDUCTIONS, Q as getReachableFiles, R as scoreGauge, Rt as deriveEvidenceLevel, S as renderSuppressions, St as createIgnoreMatcher, T as FLAKE_GLYPH, Tt as ConfigValidationError, U as headlineFor, V as wrapText, W as BADGE_BAND, X as writeFileAtomic, Y as sweepStaleTempFiles, Z as buildDependencyGraph, _t as computeCodeText, at as massCeiling, b as loadLocalRules, bt as DEFAULT_IGNORE_MATCHER, ct as getRule, dt as SCAN_ADAPTERS, et as classifyProvenance, ft as SEARCHED_FOR, gt as parseYamlGuarded, h as scan_pipeline_exports, ht as parseWorkflow, it as deductionFor, j as sectionHeader, jt as ENGINE_VERSION, k as panel, kt as isRecord$3, lt as resolveGitPath, m as runScan$1, mt as parseAzurePipeline, n as KNOWN_RULE_IDS$1, nt as capForTier, ot as RETIRED_RULE_IDS, pt as isAzurePipelineFixture, q as TRUST, rt as computeDimensions, st as RULES, tt as computeAgenticProfile, ut as runGit, v as pluginsGateOpen, vt as parseTsFile, w as sanitizeErrorText, wt as isLintFixtureDir, x as loadSuppressions, xt as LIMITS, y as renderGateNotice, yt as detectFrameworks, z as shouldColorize, zt as isAdvisoryFinding } from "./scan-pipeline-CAH9_Qgh.mjs";
|
|
3
|
+
import { accessSync, appendFileSync, chmodSync, constants, copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import process$1 from "node:process";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import { createHash
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
8
|
import ts, { ts as ts$1 } from "ts-morph";
|
|
9
9
|
import { parse } from "yaml";
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
@@ -672,27 +672,21 @@ concurrency:
|
|
|
672
672
|
|
|
673
673
|
permissions:
|
|
674
674
|
contents: read
|
|
675
|
-
pull-requests: write
|
|
676
675
|
|
|
677
676
|
jobs:
|
|
678
677
|
scan:
|
|
679
678
|
runs-on: ubuntu-latest
|
|
680
|
-
# A hung scan must not sit for the 6-hour default.
|
|
681
679
|
timeout-minutes: 10
|
|
680
|
+
permissions:
|
|
681
|
+
contents: read
|
|
682
682
|
steps:
|
|
683
683
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
684
684
|
with:
|
|
685
|
-
fetch-depth: 0
|
|
685
|
+
fetch-depth: 0
|
|
686
686
|
persist-credentials: false
|
|
687
|
-
# Scan with the pinned version. No subcommand: npx resolves
|
|
688
|
-
# the package bin directly; passing mjolnir as an arg causes
|
|
689
|
-
# npm 10+ npx to invoke it twice (unknown subcommand).
|
|
690
687
|
- name: Scan changed code (exit 1/2 is data — the gate step decides)
|
|
691
688
|
continue-on-error: true
|
|
692
689
|
run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${ENGINE_VERSION}.tgz . --scope changed --json > mjolnir.json
|
|
693
|
-
# Reporting, not gating: a crashed scan leaves mjolnir.json empty/missing
|
|
694
|
-
# and the summary step exits 2/10 — continue-on-error keeps the advisory
|
|
695
|
-
# job green, exactly like the v1 inline script did (the gate step decides).
|
|
696
690
|
- name: Annotations + Job Summary
|
|
697
691
|
if: always()
|
|
698
692
|
continue-on-error: true
|
|
@@ -700,32 +694,49 @@ jobs:
|
|
|
700
694
|
- name: Render PR comment
|
|
701
695
|
if: always()
|
|
702
696
|
continue-on-error: true
|
|
703
|
-
run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${ENGINE_VERSION}.tgz pr-comment . > mjolnir-comment.md
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
697
|
+
run: npx --yes https://registry.npmjs.org/mjolnir-qa/-/mjolnir-qa-${ENGINE_VERSION}.tgz pr-comment --from mjolnir.json > mjolnir-comment.md
|
|
698
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
699
|
+
if: always()
|
|
700
|
+
with:
|
|
701
|
+
name: mjolnir-report
|
|
702
|
+
path: mjolnir-comment.md
|
|
703
|
+
if-no-files-found: warn
|
|
704
|
+
retention-days: 30
|
|
705
|
+
${gate === "advisory" ? ` - name: Gate (advisory)
|
|
706
|
+
if: always()
|
|
707
|
+
run: echo "Advisory mode — findings reported, never blocking."` : ` - name: Gate (${gate})
|
|
708
|
+
if: always()
|
|
709
|
+
run: |
|
|
710
|
+
node -e '
|
|
711
|
+
${indentBlock(gateScript(gate), 10)}
|
|
712
|
+
'`}
|
|
713
|
+
|
|
714
|
+
publish:
|
|
715
|
+
needs: scan
|
|
716
|
+
if: always() && needs.scan.result != 'cancelled'
|
|
717
|
+
runs-on: ubuntu-latest
|
|
718
|
+
timeout-minutes: 5
|
|
719
|
+
permissions:
|
|
720
|
+
contents: read
|
|
721
|
+
pull-requests: write
|
|
722
|
+
steps:
|
|
723
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
724
|
+
with:
|
|
725
|
+
name: mjolnir-report
|
|
726
|
+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
710
727
|
if: always()
|
|
711
728
|
continue-on-error: true
|
|
712
|
-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
713
729
|
with:
|
|
714
730
|
script: |
|
|
715
731
|
const fs = require('fs');
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
const
|
|
724
|
-
owner: context.repo.owner,
|
|
725
|
-
repo: context.repo.repo,
|
|
726
|
-
issue_number: context.issue.number,
|
|
727
|
-
});
|
|
728
|
-
const existing = comments.find((c) => c.body?.startsWith(marker));
|
|
732
|
+
const body = fs.readFileSync('mjolnir-comment.md', 'utf8');
|
|
733
|
+
if (!body.trim()) return;
|
|
734
|
+
const marker = '<!-- mjolnir-report:v2 -->';
|
|
735
|
+
const comments = await github.paginate(
|
|
736
|
+
github.rest.issues.listComments,
|
|
737
|
+
{ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, per_page: 100 },
|
|
738
|
+
);
|
|
739
|
+
const existing = comments.find((comment) => comment.body?.includes(marker));
|
|
729
740
|
if (existing) {
|
|
730
741
|
await github.rest.issues.updateComment({
|
|
731
742
|
owner: context.repo.owner,
|
|
@@ -741,20 +752,9 @@ jobs:
|
|
|
741
752
|
body,
|
|
742
753
|
});
|
|
743
754
|
}
|
|
744
|
-
${gate === "advisory" ? `
|
|
745
|
-
# PR comment, never blocking. The scan step's exit code is visible
|
|
746
|
-
# as the step outcome, but continue-on-error keeps the job green.
|
|
747
|
-
- name: Gate (advisory)
|
|
748
|
-
if: always()
|
|
749
|
-
run: echo "Advisory mode — findings reported, never blocking."` : ` # Gate enforcement: the scan step's own exit code is deliberately
|
|
750
|
-
# neutralized (continue-on-error) so reporting steps always run; THIS
|
|
751
|
-
# step is what fails the job. A partial scan never blocks.
|
|
752
|
-
- name: Gate (${gate})
|
|
755
|
+
${gate === "advisory" ? ` - name: Advisory note
|
|
753
756
|
if: always()
|
|
754
|
-
run:
|
|
755
|
-
node -e '
|
|
756
|
-
${indentBlock(gateScript(gate), 10)}
|
|
757
|
-
'`}
|
|
757
|
+
run: echo "Advisory mode — findings are reported, never blocking."` : ""}
|
|
758
758
|
`;
|
|
759
759
|
/** Every gate variant, used to tell "our template" from "hand-customized". */
|
|
760
760
|
const GATES = [
|
|
@@ -793,69 +793,77 @@ concurrency:
|
|
|
793
793
|
|
|
794
794
|
permissions:
|
|
795
795
|
contents: read
|
|
796
|
-
pull-requests: write
|
|
797
796
|
|
|
798
797
|
jobs:
|
|
799
798
|
scan:
|
|
800
799
|
runs-on: ubuntu-latest
|
|
801
|
-
# A hung scan must not sit for the 6-hour default.
|
|
802
800
|
timeout-minutes: 10
|
|
801
|
+
permissions:
|
|
802
|
+
contents: read
|
|
803
803
|
steps:
|
|
804
804
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
805
805
|
with:
|
|
806
|
-
fetch-depth: 0
|
|
806
|
+
fetch-depth: 0
|
|
807
807
|
persist-credentials: false
|
|
808
|
-
# The action scans with the published mjolnir-qa package
|
|
809
|
-
# pinned to this CLI release. The composite action is pinned via
|
|
810
|
-
# ACTION_REF. The fail-on input is the gate: it fails the
|
|
811
|
-
# job on findings at the gate and never on a partial scan
|
|
812
|
-
# (exit 2 downgrades to a warning — the frozen exit-code
|
|
813
|
-
# contract). Advisory mode reports, never blocks.
|
|
814
808
|
- name: Mjölnir verification trust scan
|
|
815
809
|
id: mjolnir
|
|
816
|
-
|
|
817
|
-
continue-on-error: ${gate === "advisory" ? "true" : "false"}
|
|
810
|
+
continue-on-error: true
|
|
818
811
|
uses: ${ACTION_REF}
|
|
819
812
|
with:
|
|
820
813
|
scope: changed
|
|
821
814
|
format: json
|
|
822
815
|
fail-on: ${gate === "advisory" ? "none" : gate}
|
|
823
816
|
version: ${ENGINE_VERSION}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
817
|
+
pr-comment: "false"
|
|
818
|
+
trust-artifact: "false"
|
|
819
|
+
annotations: "true"
|
|
820
|
+
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
827
821
|
if: always()
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
822
|
+
with:
|
|
823
|
+
name: mjolnir-report
|
|
824
|
+
path: mjolnir-comment.md
|
|
825
|
+
if-no-files-found: warn
|
|
826
|
+
retention-days: 30
|
|
827
|
+
- name: Gate (${gate})
|
|
831
828
|
if: always()
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
829
|
+
shell: bash
|
|
830
|
+
env:
|
|
831
|
+
MJ_SCAN_EXIT: \${{ steps.mjolnir.outputs.exit }}
|
|
832
|
+
run: |
|
|
833
|
+
if [ "$MJ_SCAN_EXIT" = "1" ]; then
|
|
834
|
+
exit 1
|
|
835
|
+
fi
|
|
836
|
+
if [ "$MJ_SCAN_EXIT" = "2" ]; then
|
|
837
|
+
echo "::warning::Mjolnir analysis is partial; reporting remains visible but the gate is not claimed."
|
|
838
|
+
fi
|
|
839
|
+
exit 0
|
|
840
|
+
|
|
841
|
+
publish:
|
|
842
|
+
needs: scan
|
|
843
|
+
if: always() && needs.scan.result != 'cancelled'
|
|
844
|
+
runs-on: ubuntu-latest
|
|
845
|
+
timeout-minutes: 5
|
|
846
|
+
permissions:
|
|
847
|
+
contents: read
|
|
848
|
+
pull-requests: write
|
|
849
|
+
steps:
|
|
850
|
+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
851
|
+
with:
|
|
852
|
+
name: mjolnir-report
|
|
853
|
+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
840
854
|
if: always()
|
|
841
855
|
continue-on-error: true
|
|
842
|
-
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
843
856
|
with:
|
|
844
857
|
script: |
|
|
845
858
|
const fs = require('fs');
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
const
|
|
854
|
-
owner: context.repo.owner,
|
|
855
|
-
repo: context.repo.repo,
|
|
856
|
-
issue_number: context.issue.number,
|
|
857
|
-
});
|
|
858
|
-
const existing = listed.data.find((c) => c.body?.startsWith(marker));
|
|
859
|
+
const body = fs.readFileSync('mjolnir-comment.md', 'utf8');
|
|
860
|
+
if (!body.trim()) return;
|
|
861
|
+
const marker = '<!-- mjolnir-report:v2 -->';
|
|
862
|
+
const comments = await github.paginate(
|
|
863
|
+
github.rest.issues.listComments,
|
|
864
|
+
{ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, per_page: 100 },
|
|
865
|
+
);
|
|
866
|
+
const existing = comments.find((comment) => comment.body?.includes(marker));
|
|
859
867
|
if (existing) {
|
|
860
868
|
await github.rest.issues.updateComment({
|
|
861
869
|
owner: context.repo.owner,
|
|
@@ -871,17 +879,9 @@ jobs:
|
|
|
871
879
|
body,
|
|
872
880
|
});
|
|
873
881
|
}
|
|
874
|
-
${gate === "advisory" ? `
|
|
875
|
-
# never blocking — the action ran with fail-on: none and
|
|
876
|
-
# continue-on-error, so even a crashed scan cannot fail this job.
|
|
877
|
-
- name: Gate (advisory)
|
|
882
|
+
${gate === "advisory" ? ` - name: Advisory note
|
|
878
883
|
if: always()
|
|
879
|
-
run: echo "Advisory mode — findings reported, never blocking."` :
|
|
880
|
-
# ${gate} exits 1 on findings at the gate and the step is
|
|
881
|
-
# continue-on-error: false, so its failure fails the job. A partial
|
|
882
|
-
# scan never blocks (the action downgrades exit 2 to a warning per
|
|
883
|
-
# the frozen exit-code contract). The reporting steps above ran
|
|
884
|
-
# first (if: always()), so a red gate never suppresses the report.`}
|
|
884
|
+
run: echo "Advisory mode — findings are reported, never blocking."` : ""}
|
|
885
885
|
`;
|
|
886
886
|
/** Multiset line diff — counts only, for the refusal message. */
|
|
887
887
|
function summarizeContentDiff(existing, incoming) {
|
|
@@ -940,6 +940,10 @@ function digestView(f) {
|
|
|
940
940
|
trustLevel: f.trustLevel ?? null,
|
|
941
941
|
confidence: f.confidence,
|
|
942
942
|
findingType: f.findingType,
|
|
943
|
+
qaImpact: f.qaImpact,
|
|
944
|
+
findingId: f.findingId ?? null,
|
|
945
|
+
rootCauseId: f.rootCauseId ?? null,
|
|
946
|
+
deduplicationGroup: f.deduplicationGroup ?? null,
|
|
943
947
|
fixGroupId: f.fixGroupId ?? null
|
|
944
948
|
};
|
|
945
949
|
}
|
|
@@ -960,8 +964,12 @@ function canonicalScanJson(result) {
|
|
|
960
964
|
rules: result.analysisStatus.rules,
|
|
961
965
|
skippedFiles: result.analysisStatus.skippedFiles,
|
|
962
966
|
rulesCrashed: result.analysisStatus.rulesCrashed ?? 0,
|
|
963
|
-
|
|
967
|
+
parseFallbacks: result.analysisStatus.parseFallbacks ?? 0,
|
|
968
|
+
truncationReasons: result.analysisStatus.truncationReasons ?? [],
|
|
969
|
+
reasons: result.analysisStatus.reasons ?? []
|
|
964
970
|
},
|
|
971
|
+
scopeIntegrity: result.scopeIntegrity ?? null,
|
|
972
|
+
runIdentity: result.runIdentity ?? null,
|
|
965
973
|
findings: result.findings.map(digestView)
|
|
966
974
|
});
|
|
967
975
|
}
|
|
@@ -1018,9 +1026,13 @@ function buildMachineContract(result) {
|
|
|
1018
1026
|
rules: result.analysisStatus.rules,
|
|
1019
1027
|
skippedFiles: result.analysisStatus.skippedFiles,
|
|
1020
1028
|
rulesCrashed: result.analysisStatus.rulesCrashed ?? 0,
|
|
1029
|
+
...result.analysisStatus.parseFallbacks !== void 0 ? { parseFallbacks: result.analysisStatus.parseFallbacks } : {},
|
|
1021
1030
|
truncationReasons: result.analysisStatus.truncationReasons ?? [],
|
|
1031
|
+
...result.analysisStatus.reasons !== void 0 ? { reasons: result.analysisStatus.reasons } : {},
|
|
1022
1032
|
frameworkDetectionUnknown: result.frameworkDetectionUnknown,
|
|
1023
|
-
durationMs: result.analysisStatus.durationMs
|
|
1033
|
+
durationMs: result.analysisStatus.durationMs,
|
|
1034
|
+
...result.scopeIntegrity !== void 0 ? { scopeIntegrity: result.scopeIntegrity } : {},
|
|
1035
|
+
...result.runIdentity !== void 0 ? { runIdentity: result.runIdentity } : {}
|
|
1024
1036
|
},
|
|
1025
1037
|
...result.trustSummary !== void 0 ? { trustSummary: result.trustSummary } : {},
|
|
1026
1038
|
...result.agenticProfile !== void 0 ? { provenance: result.agenticProfile } : {},
|
|
@@ -1294,7 +1306,7 @@ function whatWouldChangeTheVerdict(r) {
|
|
|
1294
1306
|
/** Structural validation of the loaded JSON (hostile-input safe). */
|
|
1295
1307
|
function parseScanJson(raw) {
|
|
1296
1308
|
try {
|
|
1297
|
-
return parseJsonFile(raw, "scan result", (v) => isRecord$
|
|
1309
|
+
return parseJsonFile(raw, "scan result", (v) => isRecord$3(v) && v.schemaVersion === 1 && Array.isArray(v.findings) && Boolean(v.analysisStatus) && typeof v.partial === "boolean");
|
|
1298
1310
|
} catch {
|
|
1299
1311
|
return;
|
|
1300
1312
|
}
|
|
@@ -1409,122 +1421,6 @@ function pctOf(v) {
|
|
|
1409
1421
|
return `${Math.round(v * 100)}%`;
|
|
1410
1422
|
}
|
|
1411
1423
|
//#endregion
|
|
1412
|
-
//#region src/lib/fs-atomic.ts
|
|
1413
|
-
/**
|
|
1414
|
-
* Atomic file writes (audit S9).
|
|
1415
|
-
*
|
|
1416
|
-
* Every durability-critical write in Mjölnir (baseline, stats, badge,
|
|
1417
|
-
* TRIAGE.md, scaffolded rule files) used to hand-roll
|
|
1418
|
-
* `writeFileSync(path, data)` — a crash mid-write left a TRUNCATED file
|
|
1419
|
-
* at the real path, and a subsequent read (diff, badge endpoint) served
|
|
1420
|
-
* confident nonsense from it.
|
|
1421
|
-
*
|
|
1422
|
-
* `writeFileAtomic` writes to a temp sibling, then RENAMES. On the same
|
|
1423
|
-
* volume rename is atomic: readers see either the complete old file or
|
|
1424
|
-
* the complete new file, never a half-written one. The temp name is
|
|
1425
|
-
* created with `wx` (exclusive) so concurrent writers cannot interleave,
|
|
1426
|
-
* stale temps are cleaned up on failure, and on Windows the rename is
|
|
1427
|
-
* retried briefly because a concurrent reader can hold the destination
|
|
1428
|
-
* open (EBUSY/EPERM).
|
|
1429
|
-
*/
|
|
1430
|
-
function atomicTempPath(path) {
|
|
1431
|
-
return `${path}.mjolnir-${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}.tmp`;
|
|
1432
|
-
}
|
|
1433
|
-
/**
|
|
1434
|
-
* Atomically replace `path` with `data`.
|
|
1435
|
-
*/
|
|
1436
|
-
function writeFileAtomic(path, data, opts = {}) {
|
|
1437
|
-
const dir = dirname(path);
|
|
1438
|
-
if (opts.mkdirs !== false && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1439
|
-
const tmp = atomicTempPath(path);
|
|
1440
|
-
let fd;
|
|
1441
|
-
try {
|
|
1442
|
-
fd = openSync(tmp, "wx", opts.mode ?? 420);
|
|
1443
|
-
writeSync(fd, data, null, opts.encoding ?? "utf8");
|
|
1444
|
-
} finally {
|
|
1445
|
-
if (fd !== void 0) closeSync(fd);
|
|
1446
|
-
}
|
|
1447
|
-
try {
|
|
1448
|
-
renameWithWindowsRetry(tmp, path);
|
|
1449
|
-
} catch (err) {
|
|
1450
|
-
try {
|
|
1451
|
-
if (existsSync(tmp)) unlinkSync(tmp);
|
|
1452
|
-
} catch {}
|
|
1453
|
-
throw err;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* renameSync retry loop for Windows: a concurrent reader (another scan,
|
|
1458
|
-
* a badge endpoint, an editor) holding the destination open makes
|
|
1459
|
-
* rename fail with EBUSY/EPERM. A short bounded retry closes the race
|
|
1460
|
-
* without turning an atomic swap into a partial write.
|
|
1461
|
-
*
|
|
1462
|
-
* Internal contract test hook: the platform check keeps this loop off
|
|
1463
|
-
* the POSIX hot path; on win32 the EBUSY/EPERM arms are exercised by
|
|
1464
|
-
* the fs-atomic-retry spec (mocked renameSync).
|
|
1465
|
-
*/
|
|
1466
|
-
const RENAME_RETRIES = 8;
|
|
1467
|
-
const RENAME_RETRY_DELAY_MS = 25;
|
|
1468
|
-
/** Synchronous sleep that does not spin the CPU. */
|
|
1469
|
-
function sleepSync(ms) {
|
|
1470
|
-
try {
|
|
1471
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1472
|
-
} catch {
|
|
1473
|
-
const until = Date.now() + ms;
|
|
1474
|
-
while (Date.now() < until);
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
function renameWithWindowsRetry(from, to) {
|
|
1478
|
-
for (let attempt = 0;; attempt++) try {
|
|
1479
|
-
renameSync(from, to);
|
|
1480
|
-
return;
|
|
1481
|
-
} catch (err) {
|
|
1482
|
-
const code = err?.code;
|
|
1483
|
-
if (process.platform === "win32" && (code === "EBUSY" || code === "EPERM") && attempt < RENAME_RETRIES) {
|
|
1484
|
-
sleepSync(RENAME_RETRY_DELAY_MS);
|
|
1485
|
-
continue;
|
|
1486
|
-
}
|
|
1487
|
-
throw err;
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
const STALE_TEMP_AGE_MS = 864e5;
|
|
1491
|
-
const TEMP_PID_RE = /^.+\.mjolnir-([1-9]\d{0,9})-(\d{13})-[0-9a-f]{8}\.tmp$/;
|
|
1492
|
-
function sweepStaleTempFiles(dir) {
|
|
1493
|
-
let swept = 0;
|
|
1494
|
-
let entries;
|
|
1495
|
-
try {
|
|
1496
|
-
entries = readdirSync(dir);
|
|
1497
|
-
} catch {
|
|
1498
|
-
return 0;
|
|
1499
|
-
}
|
|
1500
|
-
const cutoff = Date.now() - STALE_TEMP_AGE_MS;
|
|
1501
|
-
for (const entry of entries) {
|
|
1502
|
-
const match = TEMP_PID_RE.exec(entry);
|
|
1503
|
-
if (!match || Number(match[2]) > cutoff) continue;
|
|
1504
|
-
const path = join(dir, entry);
|
|
1505
|
-
try {
|
|
1506
|
-
const stat = lstatSync(path);
|
|
1507
|
-
if (!stat.isFile() || stat.mtimeMs > cutoff) continue;
|
|
1508
|
-
if (pidAlive(Number(match[1]))) continue;
|
|
1509
|
-
unlinkSync(path);
|
|
1510
|
-
swept++;
|
|
1511
|
-
} catch {
|
|
1512
|
-
continue;
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
return swept;
|
|
1516
|
-
}
|
|
1517
|
-
function pidAlive(pid) {
|
|
1518
|
-
if (!Number.isSafeInteger(pid) || pid <= 0 || pid > 2147483647) return true;
|
|
1519
|
-
if (pid === process.pid) return true;
|
|
1520
|
-
try {
|
|
1521
|
-
process.kill(pid, 0);
|
|
1522
|
-
return true;
|
|
1523
|
-
} catch (err) {
|
|
1524
|
-
return err?.code !== "ESRCH";
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
//#endregion
|
|
1528
1424
|
//#region src/engine/finding-identity.ts
|
|
1529
1425
|
/**
|
|
1530
1426
|
* Finding Identity (ENGINE-002): deterministic, stable fingerprints for
|
|
@@ -2029,12 +1925,15 @@ function trustVerdictFor(v) {
|
|
|
2029
1925
|
* today the honest command set is repeat/compare/inspect, not
|
|
2030
1926
|
* "collect a trace", because trace.zip is not yet ingested.
|
|
2031
1927
|
*/
|
|
1928
|
+
function safeTriageText(value) {
|
|
1929
|
+
return value.replace(/[;&|`$()\\\r\n]/g, " ").slice(0, 200);
|
|
1930
|
+
}
|
|
2032
1931
|
function nextActionFor(v) {
|
|
2033
1932
|
switch (classifyVerdict(v)) {
|
|
2034
|
-
case "RETRY-DEPENDENT": return `repeat execution of this test (3+ runs) to confirm the flake rate, then quarantine + ticket: ${v.file}`;
|
|
2035
|
-
case "TIMEOUT": return `re-run in isolation to separate slowness from a hang:
|
|
2036
|
-
case "FAILING": return `reproduce
|
|
2037
|
-
case "SKIPPED": return `inspect the skip condition in ${v.file} — a skip after a failure usually hides an environment problem`;
|
|
1933
|
+
case "RETRY-DEPENDENT": return `repeat execution of this test (3+ runs) to confirm the flake rate, then quarantine + ticket: ${safeTriageText(v.file)}`;
|
|
1934
|
+
case "TIMEOUT": return `re-run this test in isolation with a 60-second timeout to separate slowness from a hang: ${safeTriageText(v.file)}`;
|
|
1935
|
+
case "FAILING": return `reproduce this test in isolation, then fix and re-run: ${safeTriageText(v.file)} — ${safeTriageText(v.title)}`;
|
|
1936
|
+
case "SKIPPED": return `inspect the skip condition in ${safeTriageText(v.file)} — a skip after a failure usually hides an environment problem`;
|
|
2038
1937
|
}
|
|
2039
1938
|
}
|
|
2040
1939
|
/** Deterministic guided rows, worst first (same order law as triageRows). */
|
|
@@ -2068,7 +1967,7 @@ function renderTriageWorkflow(report) {
|
|
|
2068
1967
|
}
|
|
2069
1968
|
for (const r of rows) {
|
|
2070
1969
|
const flag = r.classification === "RETRY-DEPENDENT" ? "TRUE-FLAKE" : r.classification === "FAILING" ? "FAILING" : void 0;
|
|
2071
|
-
lines.push(`[${r.classification}${flag ? ` · ${flag}` : ""}] ${r.test} (${r.file})`);
|
|
1970
|
+
lines.push(`[${r.classification}${flag ? ` · ${flag}` : ""}] ${safeTriageText(r.test)} (${safeTriageText(r.file)})`);
|
|
2072
1971
|
for (const e of r.evidence) lines.push(` evidence: ${e}`);
|
|
2073
1972
|
lines.push(` trust: ${r.trustVerdict}`);
|
|
2074
1973
|
lines.push(` next: ${r.nextAction}`);
|
|
@@ -2240,7 +2139,16 @@ function renderPwRunSummary(s) {
|
|
|
2240
2139
|
*/
|
|
2241
2140
|
/** Hard caps (§21 threat model: parameter size + resource bounds). */
|
|
2242
2141
|
const MAX_PARAM_BYTES = 65536;
|
|
2142
|
+
const MAX_DURATION_MS$1 = 36e5;
|
|
2143
|
+
const DEFAULT_MAX_DURATION_MS$1 = 6e5;
|
|
2243
2144
|
const PROTOCOL_VERSION = "2025-06-18";
|
|
2145
|
+
const CONFIGURED_ROOT = process.env.MJOLNIR_WORKSPACE_ROOT ? resolve(process.env.MJOLNIR_WORKSPACE_ROOT) : null;
|
|
2146
|
+
function isPathAllowed(path) {
|
|
2147
|
+
if (!CONFIGURED_ROOT) return true;
|
|
2148
|
+
const candidate = resolve(path);
|
|
2149
|
+
const rel = relative(CONFIGURED_ROOT, candidate);
|
|
2150
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
2151
|
+
}
|
|
2244
2152
|
/** JSON-RPC error codes (subset used here). */
|
|
2245
2153
|
const MCP_ERRORS = {
|
|
2246
2154
|
PARSE: -32700,
|
|
@@ -2359,7 +2267,10 @@ function validateParams(name, args) {
|
|
|
2359
2267
|
if (Buffer$1.byteLength(JSON.stringify(args), "utf8") > MAX_PARAM_BYTES) return `parameters exceed ${MAX_PARAM_BYTES} bytes (threat model §21)`;
|
|
2360
2268
|
if (name === "scan") {
|
|
2361
2269
|
if (typeof args["path"] !== "string" || args["path"].length === 0) return "scan requires a non-empty string `path`";
|
|
2362
|
-
if (args["maxDurationMs"] !== void 0
|
|
2270
|
+
if (args["maxDurationMs"] !== void 0) {
|
|
2271
|
+
const duration = args["maxDurationMs"];
|
|
2272
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration <= 0 || duration > MAX_DURATION_MS$1) return "`maxDurationMs` must be a finite positive number within the server budget";
|
|
2273
|
+
}
|
|
2363
2274
|
}
|
|
2364
2275
|
if (name === "explain") {
|
|
2365
2276
|
if (typeof args["ruleId"] !== "string" || !/^QA-[A-Z]+-\d{3}$/.test(args["ruleId"])) return "explain requires `ruleId` matching /^QA-[A-Z]+-\\d{3}$/";
|
|
@@ -2404,6 +2315,24 @@ async function handleToolCall(call) {
|
|
|
2404
2315
|
message: paramError
|
|
2405
2316
|
}
|
|
2406
2317
|
};
|
|
2318
|
+
const pathArgument = call.args["path"];
|
|
2319
|
+
if (typeof pathArgument === "string" && !isPathAllowed(pathArgument)) return {
|
|
2320
|
+
jsonrpc: "2.0",
|
|
2321
|
+
id: call.id,
|
|
2322
|
+
error: {
|
|
2323
|
+
code: MCP_ERRORS.INVALID_PARAMS,
|
|
2324
|
+
message: "path is outside the configured workspace root"
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
const fixturesRoot = call.args["fixturesRoot"];
|
|
2328
|
+
if (typeof fixturesRoot === "string" && !isPathAllowed(fixturesRoot)) return {
|
|
2329
|
+
jsonrpc: "2.0",
|
|
2330
|
+
id: call.id,
|
|
2331
|
+
error: {
|
|
2332
|
+
code: MCP_ERRORS.INVALID_PARAMS,
|
|
2333
|
+
message: "fixturesRoot is outside the configured workspace root"
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2407
2336
|
try {
|
|
2408
2337
|
if (call.name === "scan") {
|
|
2409
2338
|
const target = resolve(call.args["path"]);
|
|
@@ -2415,7 +2344,7 @@ async function handleToolCall(call) {
|
|
|
2415
2344
|
message: `scan target does not exist: ${target}`
|
|
2416
2345
|
}
|
|
2417
2346
|
};
|
|
2418
|
-
const maxDurationMs = typeof call.args["maxDurationMs"] === "number" ? call.args["maxDurationMs"] :
|
|
2347
|
+
const maxDurationMs = typeof call.args["maxDurationMs"] === "number" ? call.args["maxDurationMs"] : DEFAULT_MAX_DURATION_MS$1;
|
|
2419
2348
|
const result = await serializeScan(() => runScan$1({
|
|
2420
2349
|
target,
|
|
2421
2350
|
json: true,
|
|
@@ -3547,6 +3476,7 @@ const TRUST_LABELS = Object.fromEntries(TRUST_RUNGS.map((r) => [r.level, `${r.le
|
|
|
3547
3476
|
function trustHeadline(s) {
|
|
3548
3477
|
if (s.level === "L5" || s.level === "L4") return s.confidence >= .75 ? "Run evidence backs these findings — trust them." : "Run evidence exists, but the scan was incomplete — verify the gaps.";
|
|
3549
3478
|
if (s.level === "L3") return s.confidence >= .75 ? "The relevant files executed — solid static + runtime signal." : "Files executed, evidence is thin — treat findings as leads.";
|
|
3479
|
+
if (s.level === "L0") return "Static signal only, with no runtime evidence — treat as an observation, not a verdict.";
|
|
3550
3480
|
if (s.confidence >= .75) return "Deterministic static analysis — trustworthy, uncorroborated by a run.";
|
|
3551
3481
|
return "Static signal only, incomplete analysis — treat as leads, not verdicts.";
|
|
3552
3482
|
}
|
|
@@ -3554,8 +3484,10 @@ function trustHeadline(s) {
|
|
|
3554
3484
|
function trustReasons(result, s) {
|
|
3555
3485
|
const reasons = [];
|
|
3556
3486
|
const corroborated = result.findings.filter((f) => f.runtimeCorroboration !== void 0).length;
|
|
3557
|
-
if (result.findings.length === 0)
|
|
3558
|
-
|
|
3487
|
+
if (result.findings.length === 0) {
|
|
3488
|
+
const incomplete = result.partial || result.analysisStatus.discovery !== "complete" || result.analysisStatus.rules !== "complete" || s.level === "L0" && s.evidenceCoverage === 0;
|
|
3489
|
+
reasons.push(incomplete ? "No findings so far, but the scan or evidence is incomplete — the gaps are not proof of cleanliness." : "No findings fired on the analyzed surface — the scan found nothing to report.");
|
|
3490
|
+
} else reasons.push(`${result.findings.length} finding(s) fired; ${corroborated} corroborated by a real run report.`);
|
|
3559
3491
|
if (s.confidenceCeiling !== void 0) reasons.push(`confidence capped at ${pct$1(s.confidenceCeiling)} by: ${s.ceilingReasons.join(", ")}.`);
|
|
3560
3492
|
if (s.provisionalRuleIds.length > 0) reasons.push(`${s.provisionalRuleIds.length} fired rule(s) unmeasured (author-estimated FP) — disclosed, not counted.`);
|
|
3561
3493
|
else if (s.measuredFpOfFiredRules !== void 0) reasons.push(`every fired rule carries a measured false-positive rate (evidence-weighted ${pct$1(s.measuredFpOfFiredRules)}).`);
|
|
@@ -3580,7 +3512,7 @@ function topTrustRisks(findings, n = 3) {
|
|
|
3580
3512
|
}
|
|
3581
3513
|
/** NEXT ACTION — one concrete command, deterministic. */
|
|
3582
3514
|
function nextAction(result) {
|
|
3583
|
-
if (result.partial) return "re-run with a higher --max-duration to close the truncated surface";
|
|
3515
|
+
if (result.partial || result.analysisStatus.discovery !== "complete" || result.analysisStatus.rules !== "complete" || (result.analysisStatus.rulesCrashed ?? 0) > 0) return "re-run with a higher --max-duration to close the truncated surface";
|
|
3584
3516
|
const risks = topTrustRisks(result.findings, 1);
|
|
3585
3517
|
if (risks.length === 0) return result.testDeclarationCount === 0 ? "add tests — a repo without test declarations has nothing to verify" : "no findings on the analyzed surface — install advisory CI (`mjolnir ci install`) and re-scan the changed scope";
|
|
3586
3518
|
return `mjolnir explain ${risks[0]?.ruleId ?? ""} — then fix the top risk first`;
|
|
@@ -3993,6 +3925,258 @@ function currentCommit(root, opts = {}) {
|
|
|
3993
3925
|
}
|
|
3994
3926
|
}
|
|
3995
3927
|
//#endregion
|
|
3928
|
+
//#region src/commands/report-io.ts
|
|
3929
|
+
/**
|
|
3930
|
+
* Shared saved-report loading (agent-handoff plan §9.0).
|
|
3931
|
+
*
|
|
3932
|
+
* `summary`, `handoff` and `why` all consume a saved `--json` report.
|
|
3933
|
+
* One loader, one validation, one error shape — previously
|
|
3934
|
+
* validateReportJson was private to summary.ts; extracting it keeps
|
|
3935
|
+
* the three commands byte-identical in their loading behavior without
|
|
3936
|
+
* duplication. The exit-code mapping stays in the callers (they own
|
|
3937
|
+
* their io), but the error MESSAGES are identical because they come
|
|
3938
|
+
* from here.
|
|
3939
|
+
*/
|
|
3940
|
+
const MAX_REPORT_BYTES = 33554432;
|
|
3941
|
+
const MAX_FINDINGS = 1e4;
|
|
3942
|
+
const CONFIDENCE_VALUES = /* @__PURE__ */ new Set([
|
|
3943
|
+
"high",
|
|
3944
|
+
"medium",
|
|
3945
|
+
"low"
|
|
3946
|
+
]);
|
|
3947
|
+
const FINDING_TYPE_VALUES = /* @__PURE__ */ new Set([
|
|
3948
|
+
"deterministic-defect",
|
|
3949
|
+
"heuristic-risk",
|
|
3950
|
+
"observation"
|
|
3951
|
+
]);
|
|
3952
|
+
const QA_IMPACT_VALUES = /* @__PURE__ */ new Set([
|
|
3953
|
+
"BLOCKS-RELEASE",
|
|
3954
|
+
"FLAKY-RISK",
|
|
3955
|
+
"FALSE-GREEN",
|
|
3956
|
+
"HYGIENE"
|
|
3957
|
+
]);
|
|
3958
|
+
const EVIDENCE_VALUES = /* @__PURE__ */ new Set([
|
|
3959
|
+
"E0",
|
|
3960
|
+
"E1",
|
|
3961
|
+
"E2"
|
|
3962
|
+
]);
|
|
3963
|
+
const TRUST_VALUES = /* @__PURE__ */ new Set([
|
|
3964
|
+
"L0",
|
|
3965
|
+
"L1",
|
|
3966
|
+
"L2",
|
|
3967
|
+
"L3",
|
|
3968
|
+
"L4",
|
|
3969
|
+
"L5"
|
|
3970
|
+
]);
|
|
3971
|
+
/** Human message for any thrown value — never "undefined"/"[object Object]". */
|
|
3972
|
+
function errorText$1(err) {
|
|
3973
|
+
if (err instanceof Error) return err.message;
|
|
3974
|
+
if (typeof err === "string") return err;
|
|
3975
|
+
if (typeof err === "object" && err !== null) return JSON.stringify(err);
|
|
3976
|
+
return String(err);
|
|
3977
|
+
}
|
|
3978
|
+
function isRecord$2(value) {
|
|
3979
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3980
|
+
}
|
|
3981
|
+
function fail(message) {
|
|
3982
|
+
throw new Error(message);
|
|
3983
|
+
}
|
|
3984
|
+
function requireRecord(value, label) {
|
|
3985
|
+
if (!isRecord$2(value)) fail(`${label} must be an object`);
|
|
3986
|
+
return value;
|
|
3987
|
+
}
|
|
3988
|
+
function requireString(value, label) {
|
|
3989
|
+
if (typeof value !== "string" || value.length === 0) fail(`${label} must be a non-empty string`);
|
|
3990
|
+
return value;
|
|
3991
|
+
}
|
|
3992
|
+
function requireBoolean(value, label) {
|
|
3993
|
+
if (typeof value !== "boolean") fail(`${label} must be a boolean`);
|
|
3994
|
+
return value;
|
|
3995
|
+
}
|
|
3996
|
+
function requireFiniteNumber(value, label) {
|
|
3997
|
+
if (typeof value !== "number" || !Number.isFinite(value)) fail(`${label} must be a finite number`);
|
|
3998
|
+
return value;
|
|
3999
|
+
}
|
|
4000
|
+
function requireInteger(value, label, minimum = 0) {
|
|
4001
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum) fail(`${label} must be an integer >= ${minimum}`);
|
|
4002
|
+
return value;
|
|
4003
|
+
}
|
|
4004
|
+
function validateDimension(value, index) {
|
|
4005
|
+
const dimension = requireRecord(value, `dimensions[${index}]`);
|
|
4006
|
+
const category = requireString(dimension.category, `dimensions[${index}].category`);
|
|
4007
|
+
if (!RULE_CATEGORIES.includes(category)) fail(`dimensions[${index}].category is unknown`);
|
|
4008
|
+
const score = requireFiniteNumber(dimension.score, `dimensions[${index}].score`);
|
|
4009
|
+
if (score < 0 || score > 100) fail(`dimensions[${index}].score must be between 0 and 100`);
|
|
4010
|
+
requireInteger(dimension.errors, `dimensions[${index}].errors`);
|
|
4011
|
+
requireInteger(dimension.warnings, `dimensions[${index}].warnings`);
|
|
4012
|
+
requireInteger(dimension.infos, `dimensions[${index}].infos`);
|
|
4013
|
+
}
|
|
4014
|
+
function validateFinding(value, index) {
|
|
4015
|
+
const finding = requireRecord(value, `findings[${index}]`);
|
|
4016
|
+
requireString(finding.ruleId, `findings[${index}].ruleId`);
|
|
4017
|
+
const category = requireString(finding.category, `findings[${index}].category`);
|
|
4018
|
+
if (!RULE_CATEGORIES.includes(category)) fail(`findings[${index}].category is unknown`);
|
|
4019
|
+
const severity = requireString(finding.severity, `findings[${index}].severity`);
|
|
4020
|
+
if (!SEVERITY_ORDER$1.includes(severity)) fail(`findings[${index}].severity is unknown`);
|
|
4021
|
+
const confidence = requireString(finding.confidence, `findings[${index}].confidence`);
|
|
4022
|
+
if (!CONFIDENCE_VALUES.has(confidence)) fail(`findings[${index}].confidence is unknown`);
|
|
4023
|
+
const findingType = requireString(finding.findingType, `findings[${index}].findingType`);
|
|
4024
|
+
if (!FINDING_TYPE_VALUES.has(findingType)) fail(`findings[${index}].findingType is unknown`);
|
|
4025
|
+
const qaImpact = requireString(finding.qaImpact, `findings[${index}].qaImpact`);
|
|
4026
|
+
if (!QA_IMPACT_VALUES.has(qaImpact)) fail(`findings[${index}].qaImpact is unknown`);
|
|
4027
|
+
requireString(finding.file, `findings[${index}].file`);
|
|
4028
|
+
requireInteger(finding.line, `findings[${index}].line`, 1);
|
|
4029
|
+
requireInteger(finding.column, `findings[${index}].column`, 1);
|
|
4030
|
+
requireString(finding.message, `findings[${index}].message`);
|
|
4031
|
+
requireString(finding.why, `findings[${index}].why`);
|
|
4032
|
+
requireString(finding.fix, `findings[${index}].fix`);
|
|
4033
|
+
if (finding.evidenceLevel !== void 0 && (typeof finding.evidenceLevel !== "string" || !EVIDENCE_VALUES.has(finding.evidenceLevel))) fail(`findings[${index}].evidenceLevel is unknown`);
|
|
4034
|
+
if (finding.trustLevel !== void 0 && (typeof finding.trustLevel !== "string" || !TRUST_VALUES.has(finding.trustLevel))) fail(`findings[${index}].trustLevel is unknown`);
|
|
4035
|
+
if (finding.detectorRevision !== void 0) requireInteger(finding.detectorRevision, `findings[${index}].detectorRevision`, 1);
|
|
4036
|
+
if (finding.measuredFpRate !== void 0) {
|
|
4037
|
+
const rate = requireFiniteNumber(finding.measuredFpRate, `findings[${index}].measuredFpRate`);
|
|
4038
|
+
if (rate < 0 || rate > 1) fail(`findings[${index}].measuredFpRate must be between 0 and 1`);
|
|
4039
|
+
}
|
|
4040
|
+
if (finding.measuredFpN !== void 0) requireInteger(finding.measuredFpN, `findings[${index}].measuredFpN`);
|
|
4041
|
+
}
|
|
4042
|
+
function validateAnalysisStatus(value) {
|
|
4043
|
+
const status = requireRecord(value, "analysisStatus");
|
|
4044
|
+
if (status.discovery !== "complete" && status.discovery !== "partial") fail("analysisStatus.discovery must be \"complete\" or \"partial\"");
|
|
4045
|
+
if (status.rules !== "complete" && status.rules !== "partial") fail("analysisStatus.rules must be \"complete\" or \"partial\"");
|
|
4046
|
+
requireInteger(status.skippedFiles, "analysisStatus.skippedFiles");
|
|
4047
|
+
requireFiniteNumber(status.durationMs, "analysisStatus.durationMs");
|
|
4048
|
+
if (status.rulesCrashed !== void 0) requireInteger(status.rulesCrashed, "analysisStatus.rulesCrashed");
|
|
4049
|
+
if (status.parseFallbacks !== void 0) requireInteger(status.parseFallbacks, "analysisStatus.parseFallbacks");
|
|
4050
|
+
if (status.truncationReasons !== void 0) {
|
|
4051
|
+
if (!Array.isArray(status.truncationReasons)) fail("analysisStatus.truncationReasons must be an array");
|
|
4052
|
+
status.truncationReasons.forEach((reason, index) => requireString(reason, `analysisStatus.truncationReasons[${index}]`));
|
|
4053
|
+
}
|
|
4054
|
+
if (status.reasons !== void 0) {
|
|
4055
|
+
if (!Array.isArray(status.reasons)) fail("analysisStatus.reasons must be an array");
|
|
4056
|
+
status.reasons.forEach((reason, index) => requireString(reason, `analysisStatus.reasons[${index}]`));
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
function validateScopeIntegrity(value) {
|
|
4060
|
+
const scope = requireRecord(value, "scopeIntegrity");
|
|
4061
|
+
requireInteger(scope.discovered, "scopeIntegrity.discovered");
|
|
4062
|
+
requireInteger(scope.analyzed, "scopeIntegrity.analyzed");
|
|
4063
|
+
requireInteger(scope.ignored, "scopeIntegrity.ignored");
|
|
4064
|
+
requireInteger(scope.unrecognized, "scopeIntegrity.unrecognized");
|
|
4065
|
+
requireInteger(scope.parseFailed, "scopeIntegrity.parseFailed");
|
|
4066
|
+
requireInteger(scope.truncated, "scopeIntegrity.truncated");
|
|
4067
|
+
if (scope.scopeVerdict !== "PROVEN" && scope.scopeVerdict !== "PARTIAL") fail("scopeIntegrity.scopeVerdict must be \"PROVEN\" or \"PARTIAL\"");
|
|
4068
|
+
if (scope.reasons !== void 0) {
|
|
4069
|
+
if (!Array.isArray(scope.reasons)) fail("scopeIntegrity.reasons must be an array");
|
|
4070
|
+
scope.reasons.forEach((reason, index) => requireString(reason, `scopeIntegrity.reasons[${index}]`));
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
/**
|
|
4074
|
+
* Parse-and-validate a saved report. Throws Error with a
|
|
4075
|
+
* human-explanatory message on invalid JSON, shape, or contract values.
|
|
4076
|
+
*/
|
|
4077
|
+
function validateReportJson(text) {
|
|
4078
|
+
if (Buffer.byteLength(text, "utf8") > MAX_REPORT_BYTES) fail(`report exceeds the ${MAX_REPORT_BYTES}-byte limit`);
|
|
4079
|
+
let parsed;
|
|
4080
|
+
try {
|
|
4081
|
+
parsed = JSON.parse(text);
|
|
4082
|
+
} catch (err) {
|
|
4083
|
+
throw new Error(`not valid JSON (${errorText$1(err)})`, { cause: err });
|
|
4084
|
+
}
|
|
4085
|
+
const doc = requireRecord(parsed, "the report is not an object");
|
|
4086
|
+
if (doc.schemaVersion !== 1) fail(`unsupported schemaVersion ${JSON.stringify(doc.schemaVersion)} — expected schemaVersion 1`);
|
|
4087
|
+
requireBoolean(doc.partial, "partial");
|
|
4088
|
+
if (doc.score !== null) {
|
|
4089
|
+
const score = requireFiniteNumber(doc.score, "score");
|
|
4090
|
+
if (score < 0 || score > 100) fail("score must be null or between 0 and 100");
|
|
4091
|
+
}
|
|
4092
|
+
if (doc.reason !== void 0 && doc.reason !== "no-tests-found") fail("reason must be \"no-tests-found\" when present");
|
|
4093
|
+
if (doc.frameworks === void 0) fail("missing a \"frameworks\" array — is this a complete Mjölnir --json report?");
|
|
4094
|
+
if (!Array.isArray(doc.frameworks) || doc.frameworks.some((value) => typeof value !== "string")) fail("frameworks must be an array of strings");
|
|
4095
|
+
requireBoolean(doc.frameworkDetectionUnknown, "frameworkDetectionUnknown");
|
|
4096
|
+
if (!Array.isArray(doc.dimensions)) fail("dimensions must be an array");
|
|
4097
|
+
doc.dimensions.forEach(validateDimension);
|
|
4098
|
+
if (!Array.isArray(doc.findings)) fail("missing a \"findings\" array — is this a Mjölnir --json report?");
|
|
4099
|
+
if (doc.findings.length > MAX_FINDINGS) fail(`findings exceeds the ${MAX_FINDINGS}-finding limit`);
|
|
4100
|
+
doc.findings.forEach(validateFinding);
|
|
4101
|
+
validateAnalysisStatus(doc.analysisStatus);
|
|
4102
|
+
if (doc.scopeIntegrity !== void 0) validateScopeIntegrity(doc.scopeIntegrity);
|
|
4103
|
+
const status = requireRecord(doc.analysisStatus, "analysisStatus");
|
|
4104
|
+
const partialMarkers = status["discovery"] !== "complete" || status["rules"] !== "complete" || Number(status["skippedFiles"]) > 0 || Number(status["rulesCrashed"] ?? 0) > 0 || Array.isArray(status["reasons"]) && status["reasons"].length > 0;
|
|
4105
|
+
if (doc.partial === false && partialMarkers) fail("partial=false conflicts with analysisStatus");
|
|
4106
|
+
if (doc.partial === true && typeof doc.score === "number" && doc.score === 100) fail("partial reports cannot have a perfect score");
|
|
4107
|
+
if ((isRecord$2(doc.scopeIntegrity) ? doc.scopeIntegrity : void 0)?.["scopeVerdict"] === "PARTIAL" && doc.partial === false) fail("partial=false conflicts with scopeIntegrity");
|
|
4108
|
+
return parsed;
|
|
4109
|
+
}
|
|
4110
|
+
/** Load + validate a saved report from disk. Throws on any problem. */
|
|
4111
|
+
function loadSavedReport(reportPath) {
|
|
4112
|
+
const stat = statSync(reportPath);
|
|
4113
|
+
if (!stat.isFile()) fail(`report path is not a file: ${reportPath}`);
|
|
4114
|
+
if (stat.size > MAX_REPORT_BYTES) fail(`report exceeds the ${MAX_REPORT_BYTES}-byte limit`);
|
|
4115
|
+
return validateReportJson(readFileSync(reportPath, "utf8"));
|
|
4116
|
+
}
|
|
4117
|
+
/** True when the report file exists (callers own the not-found message). */
|
|
4118
|
+
function reportExists(reportPath) {
|
|
4119
|
+
return existsSync(reportPath);
|
|
4120
|
+
}
|
|
4121
|
+
//#endregion
|
|
4122
|
+
//#region src/integrations/github/evidence-sanitization.ts
|
|
4123
|
+
const MAX_MARKDOWN_TEXT_LENGTH = 500;
|
|
4124
|
+
/**
|
|
4125
|
+
* Characters that have special meaning in Markdown and must be escaped
|
|
4126
|
+
* to render as literal text.
|
|
4127
|
+
*/
|
|
4128
|
+
const MARKDOWN_SPECIAL_RE = /[[\]*_`~\\|{}()<>#!]/g;
|
|
4129
|
+
/**
|
|
4130
|
+
* HTML tags and entities that could enable injection.
|
|
4131
|
+
*/
|
|
4132
|
+
const HTML_INJECTION_RE = /<[^>]*>/g;
|
|
4133
|
+
const HTML_ENTITY_RE = /&\w+;|&#\d+;|&#x[\da-f]+;/gi;
|
|
4134
|
+
/**
|
|
4135
|
+
* Patterns that could form malformed or malicious links.
|
|
4136
|
+
*/
|
|
4137
|
+
const MALFORMED_LINK_PATTERNS = [/\]\s*\(\s*(?:javascript|data|vbscript)\s*:/gi, /\]\s*\(\s*[^)]{0,200}<[^)]{0,200}>/g];
|
|
4138
|
+
const RTL_OVERRIDE_RE = /[\u200f\u202e\u2067\u2066\u2069\u2068]/g;
|
|
4139
|
+
/**
|
|
4140
|
+
* Escape Markdown special characters in text so they render as literals.
|
|
4141
|
+
*/
|
|
4142
|
+
function escapeMarkdownChars(text) {
|
|
4143
|
+
return text.replace(MARKDOWN_SPECIAL_RE, "\\$&");
|
|
4144
|
+
}
|
|
4145
|
+
/**
|
|
4146
|
+
* Strip HTML tags and entities to prevent injection.
|
|
4147
|
+
*/
|
|
4148
|
+
function stripHtml(text) {
|
|
4149
|
+
return text.replace(HTML_ENTITY_RE, "").replace(HTML_INJECTION_RE, "");
|
|
4150
|
+
}
|
|
4151
|
+
/**
|
|
4152
|
+
* Neutralize potential malformed link patterns.
|
|
4153
|
+
*/
|
|
4154
|
+
function neutralizeMalformedLinks(text) {
|
|
4155
|
+
let result = text;
|
|
4156
|
+
for (const pattern of MALFORMED_LINK_PATTERNS) result = result.replace(pattern, (match) => `[sanitized: ${match.slice(0, 40)}]`);
|
|
4157
|
+
return result;
|
|
4158
|
+
}
|
|
4159
|
+
/**
|
|
4160
|
+
* Strip RTL override characters that could confuse display.
|
|
4161
|
+
*/
|
|
4162
|
+
function stripRtlOverrides(text) {
|
|
4163
|
+
return text.replace(RTL_OVERRIDE_RE, "");
|
|
4164
|
+
}
|
|
4165
|
+
/**
|
|
4166
|
+
* Sanitize arbitrary text for safe inclusion in Markdown.
|
|
4167
|
+
* Escapes special chars, strips HTML, prevents link injection,
|
|
4168
|
+
* strips RTL overrides, and caps length.
|
|
4169
|
+
*/
|
|
4170
|
+
function sanitizeForMarkdown(text) {
|
|
4171
|
+
let result = text;
|
|
4172
|
+
result = stripHtml(result);
|
|
4173
|
+
result = stripRtlOverrides(result);
|
|
4174
|
+
result = neutralizeMalformedLinks(result);
|
|
4175
|
+
result = escapeMarkdownChars(result);
|
|
4176
|
+
if (result.length > MAX_MARKDOWN_TEXT_LENGTH) result = result.slice(0, MAX_MARKDOWN_TEXT_LENGTH) + "\\.\\.\\.";
|
|
4177
|
+
return result;
|
|
4178
|
+
}
|
|
4179
|
+
//#endregion
|
|
3996
4180
|
//#region src/commands/trust-report.ts
|
|
3997
4181
|
/**
|
|
3998
4182
|
* `mjolnir trust-report` — the Trust Artifact (Mega MVP Master Plan
|
|
@@ -4033,7 +4217,23 @@ function fallbackSummary(result) {
|
|
|
4033
4217
|
}
|
|
4034
4218
|
/** HTML-escape a text interpolation (the artifact is hostile-input-safe). */
|
|
4035
4219
|
function esc(text) {
|
|
4036
|
-
return text.
|
|
4220
|
+
return [...text].filter((character) => {
|
|
4221
|
+
const code = character.codePointAt(0) ?? 0;
|
|
4222
|
+
return !(code <= 31 || code >= 127 && code <= 159 || code === 8207 || code === 8238 || code >= 8294 && code <= 8297);
|
|
4223
|
+
}).join("").slice(0, 1e3).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
4224
|
+
}
|
|
4225
|
+
function publicText(text) {
|
|
4226
|
+
return sanitizeForMarkdown(sanitizeErrorText(text, { maxLength: 1e3 }));
|
|
4227
|
+
}
|
|
4228
|
+
function publicJsonText(text) {
|
|
4229
|
+
return sanitizeErrorText(text, { maxLength: 1e3 });
|
|
4230
|
+
}
|
|
4231
|
+
function writeArtifact(path, content) {
|
|
4232
|
+
try {
|
|
4233
|
+
writeFileAtomic(path, content, { encoding: "utf8" });
|
|
4234
|
+
} catch (err) {
|
|
4235
|
+
throw new Error(`failed to write artifact ${path}: ${errorMessage(err)}`, { cause: err });
|
|
4236
|
+
}
|
|
4037
4237
|
}
|
|
4038
4238
|
function buildArtifactIdentity(result, commit) {
|
|
4039
4239
|
const revisions = /* @__PURE__ */ new Map();
|
|
@@ -4061,13 +4261,13 @@ function buildArtifactIdentity(result, commit) {
|
|
|
4061
4261
|
}
|
|
4062
4262
|
/** The MD identity section lines (§9 R9). */
|
|
4063
4263
|
function identityLinesMd(identity) {
|
|
4064
|
-
const revs = identity.detectorRevisions.map((r) => `\`${r.ruleId}@${r.detectorRevision}\``).join(" · ");
|
|
4264
|
+
const revs = identity.detectorRevisions.map((r) => `\`${sanitizeForMarkdown(r.ruleId)}@${r.detectorRevision}\``).join(" · ");
|
|
4065
4265
|
const levels = Object.entries(identity.evidenceInventory.byEvidenceLevel).map(([level, n]) => `${level}:${n}`).join(" · ");
|
|
4066
4266
|
return [
|
|
4067
4267
|
`## Artifact integrity`,
|
|
4068
4268
|
"",
|
|
4069
|
-
`- **scanId**: ${identity.scanId
|
|
4070
|
-
`- **Commit**: ${identity.commit
|
|
4269
|
+
`- **scanId**: ${identity.scanId ? sanitizeForMarkdown(identity.scanId) : "unbound (pre-R9 producer — regenerate)"}`,
|
|
4270
|
+
`- **Commit**: ${identity.commit ? sanitizeForMarkdown(identity.commit) : "unknown (not bound)"}`,
|
|
4071
4271
|
`- **Rule(rev) inventory**: ${revs === "" ? "none fired" : revs}`,
|
|
4072
4272
|
`- **Evidence inventory**: ${identity.evidenceInventory.totalFindings} finding(s), ${identity.evidenceInventory.corroborated} runtime-corroborated, by level ${levels === "" ? "none" : levels}.`,
|
|
4073
4273
|
""
|
|
@@ -4079,7 +4279,7 @@ function renderTrustReportMarkdown(result, label, commit) {
|
|
|
4079
4279
|
const lines = [];
|
|
4080
4280
|
lines.push(`<!-- mjolnir-trust-report:v1 -->`);
|
|
4081
4281
|
lines.push("");
|
|
4082
|
-
lines.push(`# Mjölnir Trust Report — ${label}`);
|
|
4282
|
+
lines.push(`# Mjölnir Trust Report — ${publicText(label)}`);
|
|
4083
4283
|
lines.push("");
|
|
4084
4284
|
lines.push(`> Tests tell you what passed. Mjölnir tells you what you can trust.`);
|
|
4085
4285
|
lines.push("");
|
|
@@ -4100,7 +4300,7 @@ function renderTrustReportMarkdown(result, label, commit) {
|
|
|
4100
4300
|
lines.push(`| Tests analyzed | ${result.testDeclarationCount ?? 0} in ${result.testFileCount ?? 0} files |`);
|
|
4101
4301
|
lines.push("");
|
|
4102
4302
|
if (s.ceilingReasons.length > 0) {
|
|
4103
|
-
lines.push(`Incompleteness factors: ${s.ceilingReasons.join(", ")}.`);
|
|
4303
|
+
lines.push(`Incompleteness factors: ${s.ceilingReasons.map(publicText).join(", ")}.`);
|
|
4104
4304
|
lines.push("");
|
|
4105
4305
|
}
|
|
4106
4306
|
lines.push(`## Top trust risks`);
|
|
@@ -4112,7 +4312,7 @@ function renderTrustReportMarkdown(result, label, commit) {
|
|
|
4112
4312
|
lines.push(`| ---- | -------- | -------- | ------- |`);
|
|
4113
4313
|
for (const f of risks) {
|
|
4114
4314
|
const ev = evidenceTag(f);
|
|
4115
|
-
lines.push(`| ${f.ruleId} | ${f.file}:${f.line} | ${ev} | ${f.message
|
|
4315
|
+
lines.push(`| ${publicText(f.ruleId)} | ${publicText(f.file)}:${f.line} | ${ev} | ${publicText(f.message)} |`);
|
|
4116
4316
|
}
|
|
4117
4317
|
}
|
|
4118
4318
|
lines.push("");
|
|
@@ -4126,7 +4326,7 @@ function renderTrustReportMarkdown(result, label, commit) {
|
|
|
4126
4326
|
lines.push("");
|
|
4127
4327
|
lines.push(`## Next action`);
|
|
4128
4328
|
lines.push("");
|
|
4129
|
-
lines.push(nextAction(result));
|
|
4329
|
+
lines.push(publicText(nextAction(result)));
|
|
4130
4330
|
lines.push("");
|
|
4131
4331
|
lines.push(...identityLinesMd(buildArtifactIdentity(result, commit)));
|
|
4132
4332
|
lines.push(`---`);
|
|
@@ -4138,6 +4338,7 @@ function renderTrustReportMarkdown(result, label, commit) {
|
|
|
4138
4338
|
/** Deterministic JSON artifact (agent-consumable twin of the MD). */
|
|
4139
4339
|
function renderTrustReportJson(result, commit) {
|
|
4140
4340
|
const s = fallbackSummary(result);
|
|
4341
|
+
const completionVerdict = result.partial || result.analysisStatus.discovery !== "complete" || result.analysisStatus.rules !== "complete" || (result.analysisStatus.rulesCrashed ?? 0) > 0 ? "INCOMPLETE" : result.score === null ? "UNKNOWN" : "COMPLETE";
|
|
4141
4342
|
return JSON.stringify({
|
|
4142
4343
|
schemaVersion: 1,
|
|
4143
4344
|
artifact: "mjolnir-trust-report",
|
|
@@ -4154,6 +4355,10 @@ function renderTrustReportJson(result, commit) {
|
|
|
4154
4355
|
ceilingReasons: s.ceilingReasons
|
|
4155
4356
|
},
|
|
4156
4357
|
score: result.score,
|
|
4358
|
+
partial: result.partial,
|
|
4359
|
+
analysisStatus: result.analysisStatus,
|
|
4360
|
+
scopeIntegrity: result.scopeIntegrity ?? null,
|
|
4361
|
+
verdict: completionVerdict,
|
|
4157
4362
|
tests: {
|
|
4158
4363
|
files: result.testFileCount ?? 0,
|
|
4159
4364
|
declarations: result.testDeclarationCount ?? 0
|
|
@@ -4166,14 +4371,14 @@ function renderTrustReportJson(result, commit) {
|
|
|
4166
4371
|
advisory: result.findings.filter((f) => isAdvisoryFinding(f)).length
|
|
4167
4372
|
},
|
|
4168
4373
|
topTrustRisks: topTrustRisks(result.findings, 10).map((f) => ({
|
|
4169
|
-
ruleId: f.ruleId,
|
|
4170
|
-
file: f.file,
|
|
4374
|
+
ruleId: publicJsonText(f.ruleId),
|
|
4375
|
+
file: publicJsonText(f.file),
|
|
4171
4376
|
line: f.line,
|
|
4172
4377
|
severity: f.severity,
|
|
4173
4378
|
evidence: f.runtimeCorroboration === void 0 ? f.evidenceLevel ?? "E2" : f.runtimeCorroboration.level,
|
|
4174
|
-
message: f.message
|
|
4379
|
+
message: publicJsonText(f.message)
|
|
4175
4380
|
})),
|
|
4176
|
-
nextAction: nextAction(result)
|
|
4381
|
+
nextAction: publicJsonText(nextAction(result))
|
|
4177
4382
|
}, null, 2) + "\n";
|
|
4178
4383
|
}
|
|
4179
4384
|
/**
|
|
@@ -4290,26 +4495,21 @@ async function runTrustReportCommand(argv, io) {
|
|
|
4290
4495
|
io.err("error: --commit requires the run's HEAD sha");
|
|
4291
4496
|
return 10;
|
|
4292
4497
|
}
|
|
4293
|
-
let
|
|
4294
|
-
try {
|
|
4295
|
-
raw = readFileSync(resolve(fromPath), "utf8");
|
|
4296
|
-
} catch (err) {
|
|
4297
|
-
io.err(`error: cannot read ${fromPath}: ${errorMessage(err)}`);
|
|
4298
|
-
return 10;
|
|
4299
|
-
}
|
|
4300
|
-
let parsed;
|
|
4498
|
+
let scan;
|
|
4301
4499
|
try {
|
|
4302
|
-
|
|
4500
|
+
scan = loadSavedReport(resolve(fromPath));
|
|
4303
4501
|
} catch (err) {
|
|
4304
4502
|
io.err(`error: cannot read ${fromPath}: ${errorMessage(err)}`);
|
|
4305
4503
|
return 10;
|
|
4306
4504
|
}
|
|
4307
|
-
if (
|
|
4308
|
-
io.err(`error: ${
|
|
4505
|
+
if (commitArg && scan.runIdentity?.commit && scan.runIdentity.commit !== commitArg) {
|
|
4506
|
+
io.err(`error: report commit ${scan.runIdentity.commit} does not match --commit ${commitArg}`);
|
|
4309
4507
|
return 10;
|
|
4310
4508
|
}
|
|
4311
|
-
|
|
4312
|
-
|
|
4509
|
+
if (commitArg && scan.runIdentity) scan.runIdentity = {
|
|
4510
|
+
...scan.runIdentity,
|
|
4511
|
+
commit: commitArg
|
|
4512
|
+
};
|
|
4313
4513
|
const md = renderTrustReportMarkdown(scan, fromPath, commitArg ?? null);
|
|
4314
4514
|
if (argv.includes("--stdout")) {
|
|
4315
4515
|
io.out(md);
|
|
@@ -4319,9 +4519,9 @@ async function runTrustReportCommand(argv, io) {
|
|
|
4319
4519
|
const outPath = resolve(dirname(fromPath), TRUST_REPORT_MD);
|
|
4320
4520
|
const html = renderTrustReportHtml(scan, fromPath, commitArg ?? null);
|
|
4321
4521
|
const json = renderTrustReportJson(scan, commitArg ?? null);
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4522
|
+
writeArtifact(outPath, md);
|
|
4523
|
+
writeArtifact(resolve(dirname(fromPath), TRUST_REPORT_HTML), html);
|
|
4524
|
+
writeArtifact(resolve(dirname(fromPath), TRUST_REPORT_JSON), json);
|
|
4325
4525
|
io.out(`trust report written: ${outPath} (plus ${TRUST_REPORT_HTML}, ${TRUST_REPORT_JSON} — same identity binding)`);
|
|
4326
4526
|
return 0;
|
|
4327
4527
|
} catch (err) {
|
|
@@ -4340,17 +4540,21 @@ async function runTrustReportCommand(argv, io) {
|
|
|
4340
4540
|
target,
|
|
4341
4541
|
json: true,
|
|
4342
4542
|
verbose: false,
|
|
4343
|
-
maxDurationMs:
|
|
4543
|
+
maxDurationMs: 6e5,
|
|
4344
4544
|
scopeChanged: false,
|
|
4345
4545
|
format: "json"
|
|
4346
4546
|
});
|
|
4347
4547
|
const commit = currentCommit(target, { nullable: true });
|
|
4548
|
+
if (commit && result.runIdentity) result.runIdentity = {
|
|
4549
|
+
...result.runIdentity,
|
|
4550
|
+
commit
|
|
4551
|
+
};
|
|
4348
4552
|
const md = renderTrustReportMarkdown(result, targetArg, commit);
|
|
4349
4553
|
const json = renderTrustReportJson(result, commit);
|
|
4350
4554
|
const html = renderTrustReportHtml(result, targetArg, commit);
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4555
|
+
writeArtifact(join(target, TRUST_REPORT_MD), md);
|
|
4556
|
+
writeArtifact(join(target, TRUST_REPORT_JSON), json);
|
|
4557
|
+
writeArtifact(join(target, TRUST_REPORT_HTML), html);
|
|
4354
4558
|
io.out(`trust report written: ${TRUST_REPORT_MD}, ${TRUST_REPORT_JSON}, ${TRUST_REPORT_HTML} (deterministic — same scan, same bytes)`);
|
|
4355
4559
|
return 0;
|
|
4356
4560
|
} catch (err) {
|
|
@@ -4391,10 +4595,10 @@ function findingLineMd(f) {
|
|
|
4391
4595
|
return `${icon} **${escMd(f.ruleId)}** \`${safePath}:${f.line}\` — ${escMd(f.message)} ${escMd(`[${evidenceTagLine(f)}]`)}\n Fix: ${fixBody}`;
|
|
4392
4596
|
}
|
|
4393
4597
|
/** Score cell with color-coded emoji and optional delta. */
|
|
4394
|
-
function renderScoreBadge(score, delta) {
|
|
4598
|
+
function renderScoreBadge(score, delta, verdictOverride) {
|
|
4395
4599
|
if (score === null) return "### Score\n*n/a*";
|
|
4396
4600
|
deriveScoreState(score);
|
|
4397
|
-
let text = `**${score}**/100 ${verdictFor(score)}`;
|
|
4601
|
+
let text = `**${score}**/100 ${verdictOverride ?? verdictFor(score)}`;
|
|
4398
4602
|
if (delta !== void 0 && delta !== 0) text += ` (${delta > 0 ? "+" : ""}${delta})`;
|
|
4399
4603
|
return `### Score\n${text}`;
|
|
4400
4604
|
}
|
|
@@ -4462,6 +4666,10 @@ function renderArtifactIntegrity(identity) {
|
|
|
4462
4666
|
lines.push("</details>");
|
|
4463
4667
|
return lines;
|
|
4464
4668
|
}
|
|
4669
|
+
function isIncompleteResult(result) {
|
|
4670
|
+
const status = result.analysisStatus;
|
|
4671
|
+
return result.partial || status === void 0 || status.discovery !== "complete" || status.rules !== "complete" || (status.rulesCrashed ?? 0) > 0 || result.scopeIntegrity?.scopeVerdict === "PARTIAL";
|
|
4672
|
+
}
|
|
4465
4673
|
/**
|
|
4466
4674
|
* Render the unified "Mjölnir Verification Report" PR comment.
|
|
4467
4675
|
*
|
|
@@ -4479,7 +4687,8 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
4479
4687
|
provisionalRuleIds: [],
|
|
4480
4688
|
ceilingReasons: []
|
|
4481
4689
|
};
|
|
4482
|
-
|
|
4690
|
+
const incomplete = isIncompleteResult(result) || !result.trustSummary || summary.level === "L0" && summary.evidenceCoverage === 0;
|
|
4691
|
+
lines.push("## <img width=\"16\" alt=\"Mjölnir\" src=\"https://raw.githubusercontent.com/Sergey-Bar/Mjolnir/main/assets/mjolnir-icon.svg\"/> Mjölnir Verification Report");
|
|
4483
4692
|
lines.push("");
|
|
4484
4693
|
lines.push("> Tests tell you what passed. Mjölnir tells you what you can trust.");
|
|
4485
4694
|
lines.push("");
|
|
@@ -4487,6 +4696,11 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
4487
4696
|
lines.push("_Scope: only the lines this PR changed._");
|
|
4488
4697
|
lines.push("");
|
|
4489
4698
|
}
|
|
4699
|
+
if (incomplete) {
|
|
4700
|
+
const reasons = result.analysisStatus?.reasons ?? [];
|
|
4701
|
+
lines.push(`> **Analysis status: INCOMPLETE.** ${reasons.length > 0 ? reasons.map((reason) => escMd(reason)).join(", ") : "The scan did not complete a trustworthy analysis."}`);
|
|
4702
|
+
lines.push("");
|
|
4703
|
+
}
|
|
4490
4704
|
const diff = options.diff;
|
|
4491
4705
|
const usingDiff = diff?.hasBaseline === true;
|
|
4492
4706
|
const findings = usingDiff ? diff.newFindings : result.findings;
|
|
@@ -4494,7 +4708,7 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
4494
4708
|
const warningCount = findings.filter((f) => f.severity === "warning").length;
|
|
4495
4709
|
const baseScore = diff?.baselineScore;
|
|
4496
4710
|
const delta = baseScore !== void 0 && result.score !== null ? result.score - baseScore : void 0;
|
|
4497
|
-
lines.push("<table>");
|
|
4711
|
+
lines.push("<table role=\"presentation\">");
|
|
4498
4712
|
lines.push("<tr>");
|
|
4499
4713
|
lines.push("<td width=\"130\">");
|
|
4500
4714
|
lines.push("");
|
|
@@ -4503,7 +4717,7 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
4503
4717
|
lines.push("</td>");
|
|
4504
4718
|
lines.push("<td width=\"130\">");
|
|
4505
4719
|
lines.push("");
|
|
4506
|
-
lines.push(renderScoreBadge(result.score, delta));
|
|
4720
|
+
lines.push(renderScoreBadge(result.score, delta, incomplete ? "INCOMPLETE" : void 0));
|
|
4507
4721
|
lines.push("");
|
|
4508
4722
|
lines.push("</td>");
|
|
4509
4723
|
lines.push("<td width=\"130\">");
|
|
@@ -4521,7 +4735,7 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
4521
4735
|
lines.push("");
|
|
4522
4736
|
const state = deriveScoreState(result.score);
|
|
4523
4737
|
const headlineFindings = usingDiff ? findings.length : result.findings.length;
|
|
4524
|
-
lines.push(`**Headline:** ${headlineFor(state, headlineFindings)}`);
|
|
4738
|
+
lines.push(incomplete ? "**Headline:** Analysis is incomplete; this report is not a clean or worthy verdict." : `**Headline:** ${headlineFor(state, headlineFindings)}`);
|
|
4525
4739
|
lines.push("");
|
|
4526
4740
|
lines.push("---");
|
|
4527
4741
|
lines.push("");
|
|
@@ -4534,7 +4748,7 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
4534
4748
|
lines.push("");
|
|
4535
4749
|
}
|
|
4536
4750
|
if (findings.length === 0) {
|
|
4537
|
-
lines.push("✅ No new findings in this PR's changes.");
|
|
4751
|
+
lines.push(incomplete ? "⚠️ No findings were observed, but the analysis is incomplete; this is not proof of cleanliness." : usingDiff ? "✅ No new findings in this PR's changes." : "✅ No new findings in the analyzed surface.");
|
|
4538
4752
|
lines.push("");
|
|
4539
4753
|
} else {
|
|
4540
4754
|
const errors = findings.filter((f) => f.severity === "error");
|
|
@@ -4599,7 +4813,7 @@ function renderUnifiedReport(result, options = {}) {
|
|
|
4599
4813
|
lines.push("```");
|
|
4600
4814
|
lines.push("");
|
|
4601
4815
|
const repoLink = options.repoUrl ? ` — ${options.repoUrl}` : "";
|
|
4602
|
-
lines.push(`<sub>Posted by GitHub Actions${repoLink} — blocks merging when the CI gate reports findings at the configured severity.</sub>`);
|
|
4816
|
+
lines.push(incomplete ? `<sub>Posted by GitHub Actions${repoLink} — analysis incomplete; no release verdict is claimed.</sub>` : `<sub>Posted by GitHub Actions${repoLink} — blocks merging when the CI gate reports findings at the configured severity.</sub>`);
|
|
4603
4817
|
return lines.join("\n");
|
|
4604
4818
|
}
|
|
4605
4819
|
//#endregion
|
|
@@ -4626,57 +4840,6 @@ function renderPrComment(result, options = {}) {
|
|
|
4626
4840
|
});
|
|
4627
4841
|
}
|
|
4628
4842
|
//#endregion
|
|
4629
|
-
//#region src/commands/report-io.ts
|
|
4630
|
-
/**
|
|
4631
|
-
* Shared saved-report loading (agent-handoff plan §9.0).
|
|
4632
|
-
*
|
|
4633
|
-
* `summary`, `handoff` and `why` all consume a saved `--json` report.
|
|
4634
|
-
* One loader, one validation, one error shape — previously
|
|
4635
|
-
* validateReportJson was private to summary.ts; extracting it keeps
|
|
4636
|
-
* the three commands byte-identical in their loading behavior without
|
|
4637
|
-
* duplication. The exit-code mapping stays in the callers (they own
|
|
4638
|
-
* their io), but the error MESSAGES are identical because they come
|
|
4639
|
-
* from here.
|
|
4640
|
-
*/
|
|
4641
|
-
/** Human message for any thrown value — never "undefined"/"[object Object]". */
|
|
4642
|
-
function errorText$1(err) {
|
|
4643
|
-
if (err instanceof Error) return err.message;
|
|
4644
|
-
if (typeof err === "string") return err;
|
|
4645
|
-
if (typeof err === "object" && err !== null) return JSON.stringify(err);
|
|
4646
|
-
return String(err);
|
|
4647
|
-
}
|
|
4648
|
-
/**
|
|
4649
|
-
* Parse-and-validate a saved report. Throws Error with a
|
|
4650
|
-
* human-explanatory message on: invalid JSON, non-object document,
|
|
4651
|
-
* wrong schemaVersion, missing findings array, missing frameworks
|
|
4652
|
-
* array (certification F1: a schema-1 report is MORE than
|
|
4653
|
-
* `{schemaVersion, findings}` — summary/handoff/why read
|
|
4654
|
-
* `result.frameworks`, so a schema-incomplete stub must be rejected
|
|
4655
|
-
* HERE, by the one shared loader, not crash downstream).
|
|
4656
|
-
*/
|
|
4657
|
-
function validateReportJson(text) {
|
|
4658
|
-
let parsed;
|
|
4659
|
-
try {
|
|
4660
|
-
parsed = JSON.parse(text);
|
|
4661
|
-
} catch (err) {
|
|
4662
|
-
throw new Error(`not valid JSON (${errorText$1(err)})`, { cause: err });
|
|
4663
|
-
}
|
|
4664
|
-
if (typeof parsed !== "object" || parsed === null) throw new Error("the file is a JSON value but not an object");
|
|
4665
|
-
const doc = parsed;
|
|
4666
|
-
if (doc.schemaVersion !== 1) throw new Error(`unsupported schemaVersion ${JSON.stringify(doc.schemaVersion)} — expected 1`);
|
|
4667
|
-
if (!Array.isArray(doc.findings)) throw new Error("missing a \"findings\" array — is this a Mjölnir --json report?");
|
|
4668
|
-
if (!Array.isArray(doc.frameworks)) throw new Error("missing a \"frameworks\" array — is this a complete Mjölnir --json report?");
|
|
4669
|
-
return parsed;
|
|
4670
|
-
}
|
|
4671
|
-
/** Load + validate a saved report from disk. Throws on any problem. */
|
|
4672
|
-
function loadSavedReport(reportPath) {
|
|
4673
|
-
return validateReportJson(readFileSync(reportPath, "utf8"));
|
|
4674
|
-
}
|
|
4675
|
-
/** True when the report file exists (callers own the not-found message). */
|
|
4676
|
-
function reportExists(reportPath) {
|
|
4677
|
-
return existsSync(reportPath);
|
|
4678
|
-
}
|
|
4679
|
-
//#endregion
|
|
4680
4843
|
//#region src/commands/why.ts
|
|
4681
4844
|
/**
|
|
4682
4845
|
* `mjolnir why <file>:<line>` — occurrence-level evidence/explanation
|
|
@@ -5108,22 +5271,11 @@ function buildBadge(result, commit) {
|
|
|
5108
5271
|
* (the falsifiable claim: "verified at commit X on date Y").
|
|
5109
5272
|
*/
|
|
5110
5273
|
function renderBadgeSnippet(result, repoUrl = "https://github.com/Sergey-Bar/Mjolnir") {
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
"HEAD"
|
|
5117
|
-
], {
|
|
5118
|
-
cwd: process.cwd(),
|
|
5119
|
-
encoding: "utf8",
|
|
5120
|
-
stdio: [
|
|
5121
|
-
"ignore",
|
|
5122
|
-
"pipe",
|
|
5123
|
-
"ignore"
|
|
5124
|
-
]
|
|
5125
|
-
}).trim();
|
|
5126
|
-
} catch {}
|
|
5274
|
+
const commit = runGit(process.cwd(), [
|
|
5275
|
+
"rev-parse",
|
|
5276
|
+
"--short",
|
|
5277
|
+
"HEAD"
|
|
5278
|
+
])?.trim() ?? "unknown";
|
|
5127
5279
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
5128
5280
|
const errors = result.findings.filter((f) => f.severity === "error").length;
|
|
5129
5281
|
return [
|
|
@@ -5378,7 +5530,7 @@ function createRuleScaffold(input, rootDir) {
|
|
|
5378
5530
|
};
|
|
5379
5531
|
mkdirSync(join(rootDir, "src", "rules", parsed.dir), { recursive: true });
|
|
5380
5532
|
const files = [];
|
|
5381
|
-
|
|
5533
|
+
const ruleSrc = `/**
|
|
5382
5534
|
* ${input.id} — ${input.title}.
|
|
5383
5535
|
*
|
|
5384
5536
|
* TODO(implement): replace the placeholder below. The rule currently
|
|
@@ -5404,7 +5556,8 @@ export const ${camel(parsed.lower)} = defineRule({
|
|
|
5404
5556
|
return findings;
|
|
5405
5557
|
},
|
|
5406
5558
|
});
|
|
5407
|
-
|
|
5559
|
+
`;
|
|
5560
|
+
writeFileAtomic(absRule, ruleSrc);
|
|
5408
5561
|
files.push(ruleRel);
|
|
5409
5562
|
for (const direction of ["must-fire", "must-not-fire"]) {
|
|
5410
5563
|
const dirRel = join("tests", "fixtures", input.id, direction);
|
|
@@ -5550,13 +5703,15 @@ function renderHandover(map) {
|
|
|
5550
5703
|
* tests/privacy-network-isolation.spec.ts, which scans this file too).
|
|
5551
5704
|
*/
|
|
5552
5705
|
const ui$13 = plainContext();
|
|
5553
|
-
/** The S1-resolved absolute git binary, or
|
|
5706
|
+
/** The S1-resolved absolute git binary, or null when none is available. */
|
|
5554
5707
|
function gitExe() {
|
|
5555
|
-
return resolveGitPath()
|
|
5708
|
+
return resolveGitPath();
|
|
5556
5709
|
}
|
|
5557
5710
|
function git(root, args) {
|
|
5711
|
+
const executable = gitExe();
|
|
5712
|
+
if (!executable) return null;
|
|
5558
5713
|
try {
|
|
5559
|
-
return execFileSync(
|
|
5714
|
+
return execFileSync(executable, [
|
|
5560
5715
|
"-C",
|
|
5561
5716
|
root,
|
|
5562
5717
|
...args
|
|
@@ -5575,8 +5730,10 @@ function git(root, args) {
|
|
|
5575
5730
|
}
|
|
5576
5731
|
/** Like git(), but returns the raw bytes — no utf8 decode round-trip. */
|
|
5577
5732
|
function gitBuffer(root, args) {
|
|
5733
|
+
const executable = gitExe();
|
|
5734
|
+
if (!executable) return null;
|
|
5578
5735
|
try {
|
|
5579
|
-
return execFileSync(
|
|
5736
|
+
return execFileSync(executable, [
|
|
5580
5737
|
"-C",
|
|
5581
5738
|
root,
|
|
5582
5739
|
...args
|
|
@@ -5610,7 +5767,16 @@ async function computeImpact(root, options) {
|
|
|
5610
5767
|
"HEAD",
|
|
5611
5768
|
options.baseBranch ?? "main"
|
|
5612
5769
|
])?.trim() ?? void 0;
|
|
5613
|
-
else
|
|
5770
|
+
else {
|
|
5771
|
+
baseRef = git(root, ["rev-parse", baseRef])?.trim();
|
|
5772
|
+
if (!baseRef) return {
|
|
5773
|
+
hasComparison: false,
|
|
5774
|
+
unknownReason: "base-ref-invalid",
|
|
5775
|
+
resolved: [],
|
|
5776
|
+
introduced: [],
|
|
5777
|
+
unknownFacts
|
|
5778
|
+
};
|
|
5779
|
+
}
|
|
5614
5780
|
if (!baseRef) return {
|
|
5615
5781
|
hasComparison: false,
|
|
5616
5782
|
unknownReason: "no-prior-commit",
|
|
@@ -5618,7 +5784,15 @@ async function computeImpact(root, options) {
|
|
|
5618
5784
|
introduced: [],
|
|
5619
5785
|
unknownFacts
|
|
5620
5786
|
};
|
|
5621
|
-
const headRef = git(root, ["rev-parse", "HEAD"])?.trim()
|
|
5787
|
+
const headRef = git(root, ["rev-parse", "HEAD"])?.trim();
|
|
5788
|
+
if (!headRef) return {
|
|
5789
|
+
hasComparison: false,
|
|
5790
|
+
unknownReason: "head-ref-invalid",
|
|
5791
|
+
baseRef,
|
|
5792
|
+
resolved: [],
|
|
5793
|
+
introduced: [],
|
|
5794
|
+
unknownFacts
|
|
5795
|
+
};
|
|
5622
5796
|
if (baseRef === headRef) return {
|
|
5623
5797
|
hasComparison: false,
|
|
5624
5798
|
unknownReason: options.since ? "base-equals-head" : "no-prior-commit",
|
|
@@ -5979,7 +6153,7 @@ function tryReadPackageJson(rootDir) {
|
|
|
5979
6153
|
const p = join(rootDir, "package.json");
|
|
5980
6154
|
if (!existsSync(p)) return null;
|
|
5981
6155
|
try {
|
|
5982
|
-
return parseJsonFile(readFileSync(p, "utf8"), p, isRecord$
|
|
6156
|
+
return parseJsonFile(readFileSync(p, "utf8"), p, isRecord$3);
|
|
5983
6157
|
} catch {
|
|
5984
6158
|
return null;
|
|
5985
6159
|
}
|
|
@@ -7217,6 +7391,33 @@ async function runPrCommentCommand(argv, io = {
|
|
|
7217
7391
|
out,
|
|
7218
7392
|
err
|
|
7219
7393
|
}) {
|
|
7394
|
+
const fromIdx = argv.indexOf("--from");
|
|
7395
|
+
if (fromIdx !== -1) {
|
|
7396
|
+
const fromPath = argv[fromIdx + 1];
|
|
7397
|
+
if (!fromPath || fromPath.startsWith("-")) {
|
|
7398
|
+
io.err("error: --from requires a saved report path");
|
|
7399
|
+
return 10;
|
|
7400
|
+
}
|
|
7401
|
+
try {
|
|
7402
|
+
const saved = loadSavedReport(resolve(fromPath));
|
|
7403
|
+
const option = (flag) => {
|
|
7404
|
+
const index = argv.indexOf(flag);
|
|
7405
|
+
return index >= 0 ? argv[index + 1] : void 0;
|
|
7406
|
+
};
|
|
7407
|
+
const version = option("--version");
|
|
7408
|
+
const commit = option("--commit");
|
|
7409
|
+
const repoUrl = option("--repo-url");
|
|
7410
|
+
io.out(renderPrComment(saved, {
|
|
7411
|
+
...version ? { version } : {},
|
|
7412
|
+
...commit ? { commit } : {},
|
|
7413
|
+
...repoUrl ? { repoUrl } : {}
|
|
7414
|
+
}));
|
|
7415
|
+
return 0;
|
|
7416
|
+
} catch (err) {
|
|
7417
|
+
io.err(`error: cannot read ${fromPath}: ${errorText$1(err)}`);
|
|
7418
|
+
return 10;
|
|
7419
|
+
}
|
|
7420
|
+
}
|
|
7220
7421
|
const args = parseArgsOrUsage(argv, io);
|
|
7221
7422
|
if (!args) return 10;
|
|
7222
7423
|
try {
|
|
@@ -7376,7 +7577,12 @@ async function runRulesCommand(argv, io = {
|
|
|
7376
7577
|
return 0;
|
|
7377
7578
|
}
|
|
7378
7579
|
const root = process$1.cwd();
|
|
7379
|
-
const
|
|
7580
|
+
const gateOpen = pluginsGateOpen(argv.includes("--enable-plugins"));
|
|
7581
|
+
const external = withExternal ? await loadLocalRules(root, gateOpen) : void 0;
|
|
7582
|
+
if (external?.skipped.length) io.err(renderGateNotice(external.skipped.map((name) => ({
|
|
7583
|
+
kind: "js-module",
|
|
7584
|
+
name
|
|
7585
|
+
}))));
|
|
7380
7586
|
let catalog = [...buildCatalog(), ...external ? buildCatalog(external.rules, { provenance: "external" }) : []];
|
|
7381
7587
|
for (const w of external?.errors ?? []) io.err(`mjolnir: ${w}`);
|
|
7382
7588
|
if (argv.includes("--unmeasured")) catalog = catalog.filter((e) => e.measuredFpRate === void 0);
|
|
@@ -7701,7 +7907,7 @@ function computeDetectorHashes(rules, rulesDir, retiredIds = []) {
|
|
|
7701
7907
|
return manifest;
|
|
7702
7908
|
}
|
|
7703
7909
|
function loadManifest(path) {
|
|
7704
|
-
return parseJsonFile(readFileSync(path, "utf8"), path, (v) => isRecord$
|
|
7910
|
+
return parseJsonFile(readFileSync(path, "utf8"), path, (v) => isRecord$3(v));
|
|
7705
7911
|
}
|
|
7706
7912
|
//#endregion
|
|
7707
7913
|
//#region src/commands/doctor.ts
|
|
@@ -7878,7 +8084,7 @@ function checkTierEnforcement(verdictsDir, rules = RULES) {
|
|
|
7878
8084
|
const lines = readFileSync(join(verdictsDir, f), "utf8").split("\n").filter((l) => l.trim().length > 0);
|
|
7879
8085
|
for (const line of lines) try {
|
|
7880
8086
|
const parsed = JSON.parse(line);
|
|
7881
|
-
if (!isRecord$
|
|
8087
|
+
if (!isRecord$3(parsed)) continue;
|
|
7882
8088
|
const entry = parsed;
|
|
7883
8089
|
if (entry.verdict === "TP" || entry.verdict === "FP") {
|
|
7884
8090
|
if (typeof entry.ruleId === "string") live.set(entry.ruleId, (live.get(entry.ruleId) ?? 0) + 1);
|
|
@@ -7986,7 +8192,7 @@ function checkFixtureIntegrity(fixturesRoot, rules = RULES) {
|
|
|
7986
8192
|
const allowlistPath = join(fixturesRoot, "typecheck-allowlist.json");
|
|
7987
8193
|
if (existsSync(allowlistPath)) try {
|
|
7988
8194
|
const raw = JSON.parse(readFileSync(allowlistPath, "utf8"));
|
|
7989
|
-
if (!isRecord$
|
|
8195
|
+
if (!isRecord$3(raw)) {
|
|
7990
8196
|
ok = false;
|
|
7991
8197
|
details.push("typecheck-allowlist.json is not an object");
|
|
7992
8198
|
} else if (Array.isArray(raw.entries)) details.push(`Layer A typecheck allowlist: ${raw.entries.length} justified entr(ies)`);
|
|
@@ -8100,7 +8306,7 @@ function checkMeasurementConsistency(verdictsDir, sidecarPath, rules = RULES, me
|
|
|
8100
8306
|
let row;
|
|
8101
8307
|
try {
|
|
8102
8308
|
const parsed = JSON.parse(trimmed);
|
|
8103
|
-
if (!isRecord$
|
|
8309
|
+
if (!isRecord$3(parsed)) {
|
|
8104
8310
|
failures.push(`${f}: unparseable verdict row (corpus integrity)`);
|
|
8105
8311
|
continue;
|
|
8106
8312
|
}
|
|
@@ -8117,7 +8323,7 @@ function checkMeasurementConsistency(verdictsDir, sidecarPath, rules = RULES, me
|
|
|
8117
8323
|
let sidecar = {};
|
|
8118
8324
|
if (existsSync(sidecarPath)) try {
|
|
8119
8325
|
const parsed = JSON.parse(readFileSync(sidecarPath, "utf8"));
|
|
8120
|
-
if (!isRecord$
|
|
8326
|
+
if (!isRecord$3(parsed)) failures.push("sidecar is not a valid object — regenerate the measurement artifacts");
|
|
8121
8327
|
else sidecar = parsed;
|
|
8122
8328
|
} catch (e) {
|
|
8123
8329
|
failures.push(`sidecar unreadable/malformed: ${errorText(e)} — regenerate the measurement artifacts`);
|
|
@@ -9101,19 +9307,12 @@ function hookBlock(version) {
|
|
|
9101
9307
|
function resolveHookTarget(cwd) {
|
|
9102
9308
|
const huskyDir = join(cwd, ".husky");
|
|
9103
9309
|
if (existsSync(huskyDir)) return join(huskyDir, "pre-commit");
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
], { stdio: [
|
|
9111
|
-
"ignore",
|
|
9112
|
-
"pipe",
|
|
9113
|
-
"ignore"
|
|
9114
|
-
] }).toString().trim();
|
|
9115
|
-
return join(cwd, hooksPath, "pre-commit");
|
|
9116
|
-
} catch {}
|
|
9310
|
+
const hooksPath = runGit(cwd, ["config", "core.hooksPath"])?.trim();
|
|
9311
|
+
if (hooksPath) {
|
|
9312
|
+
const candidate = resolve(cwd, hooksPath);
|
|
9313
|
+
const relativePath = relative(cwd, candidate);
|
|
9314
|
+
if (relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath)) return join(candidate, "pre-commit");
|
|
9315
|
+
}
|
|
9117
9316
|
return join(cwd, ".git", "hooks", "pre-commit");
|
|
9118
9317
|
}
|
|
9119
9318
|
/**
|
|
@@ -9619,7 +9818,7 @@ function checkArtifactIntegrity(root) {
|
|
|
9619
9818
|
what: "the artifact embeds its identity binding (scanId/commit/rule(rev)/evidence inventory)"
|
|
9620
9819
|
},
|
|
9621
9820
|
{
|
|
9622
|
-
ok: report.includes("TRUST_REPORT_HTML") && report.includes("renderTrustReportHtml") && /
|
|
9821
|
+
ok: report.includes("TRUST_REPORT_HTML") && report.includes("renderTrustReportHtml") && /write(?:FileSync|Artifact)\(join\(target,\s*TRUST_REPORT_HTML\)/.test(report),
|
|
9623
9822
|
what: "the HTML artifact ships and the command writes all three formats"
|
|
9624
9823
|
},
|
|
9625
9824
|
{
|
|
@@ -10288,12 +10487,19 @@ async function runReportPlaywrightCommand(argv, io) {
|
|
|
10288
10487
|
io.err(`mjolnir report playwright: target does not exist: ${target}`);
|
|
10289
10488
|
return 10;
|
|
10290
10489
|
}
|
|
10490
|
+
const root = resolve(target);
|
|
10491
|
+
const requestedOutput = resolve(outputPath);
|
|
10492
|
+
const outputRelative = relative(root, requestedOutput);
|
|
10493
|
+
if (isAbsolute(outputRelative) || outputRelative.startsWith("..")) {
|
|
10494
|
+
io.err("mjolnir report playwright: output must stay within the target root");
|
|
10495
|
+
return 10;
|
|
10496
|
+
}
|
|
10291
10497
|
try {
|
|
10292
10498
|
const result = await runScan$1({
|
|
10293
10499
|
target,
|
|
10294
10500
|
json: true,
|
|
10295
10501
|
verbose: false,
|
|
10296
|
-
maxDurationMs:
|
|
10502
|
+
maxDurationMs: 6e5,
|
|
10297
10503
|
scopeChanged: false,
|
|
10298
10504
|
format: "json",
|
|
10299
10505
|
strict: false
|
|
@@ -10301,8 +10507,8 @@ async function runReportPlaywrightCommand(argv, io) {
|
|
|
10301
10507
|
const report = buildPlaywrightReport(result);
|
|
10302
10508
|
const outDir = resolve(".");
|
|
10303
10509
|
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
10304
|
-
const fullPath =
|
|
10305
|
-
|
|
10510
|
+
const fullPath = requestedOutput;
|
|
10511
|
+
writeFileAtomic(fullPath, JSON.stringify(report, null, 2) + "\n", { encoding: "utf8" });
|
|
10306
10512
|
const header = sectionHeader("PLAYWRIGHT REPORT", ui$6);
|
|
10307
10513
|
io.out(`${header}\n`);
|
|
10308
10514
|
io.out(`Score: ${result.score !== null ? result.score + "/100" : "unknown"}`);
|
|
@@ -10787,20 +10993,44 @@ function runAnalyzeCommand(argv, io) {
|
|
|
10787
10993
|
const findings = [];
|
|
10788
10994
|
const fileImports = /* @__PURE__ */ new Map();
|
|
10789
10995
|
const fileContents = /* @__PURE__ */ new Map();
|
|
10790
|
-
|
|
10996
|
+
let limited = false;
|
|
10997
|
+
const deadline = Date.now() + 6e5;
|
|
10998
|
+
function walk(dir, depth = 0) {
|
|
10999
|
+
if (limited || depth > LIMITS.maxDepth || Date.now() >= deadline) {
|
|
11000
|
+
limited = true;
|
|
11001
|
+
return;
|
|
11002
|
+
}
|
|
10791
11003
|
for (const name of readdirSync(dir)) {
|
|
10792
11004
|
const path = join(dir, name);
|
|
10793
11005
|
if (name === "node_modules" || name === ".git") continue;
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
11006
|
+
let stat;
|
|
11007
|
+
try {
|
|
11008
|
+
stat = lstatSync(path);
|
|
11009
|
+
} catch {
|
|
11010
|
+
continue;
|
|
11011
|
+
}
|
|
11012
|
+
if (stat.isSymbolicLink()) continue;
|
|
11013
|
+
if (stat.isDirectory()) {
|
|
11014
|
+
walk(path, depth + 1);
|
|
11015
|
+
if (limited) return;
|
|
11016
|
+
} else if (name.endsWith(".ts") || name.endsWith(".tsx") || name.endsWith(".js") || name.endsWith(".jsx")) {
|
|
11017
|
+
if (fileContents.size >= LIMITS.maxFilesPerAdapter) {
|
|
11018
|
+
limited = true;
|
|
11019
|
+
return;
|
|
11020
|
+
}
|
|
11021
|
+
const read = readFileBounded(path, LIMITS.maxFileBytes);
|
|
11022
|
+
if (!read.ok) {
|
|
11023
|
+
limited = true;
|
|
11024
|
+
continue;
|
|
11025
|
+
}
|
|
11026
|
+
const content = read.data.toString("utf8");
|
|
10797
11027
|
const rel = relative(process.cwd(), path);
|
|
10798
11028
|
fileContents.set(rel, content);
|
|
10799
11029
|
const imports = collectImports(content);
|
|
10800
11030
|
const importCounts = /* @__PURE__ */ new Map();
|
|
10801
11031
|
for (const imp of imports) importCounts.set(imp, (importCounts.get(imp) ?? 0) + 1);
|
|
10802
11032
|
fileImports.set(rel, importCounts);
|
|
10803
|
-
}
|
|
11033
|
+
}
|
|
10804
11034
|
}
|
|
10805
11035
|
}
|
|
10806
11036
|
walk(target);
|
|
@@ -10854,6 +11084,10 @@ function runAnalyzeCommand(argv, io) {
|
|
|
10854
11084
|
}
|
|
10855
11085
|
io.out("");
|
|
10856
11086
|
}
|
|
11087
|
+
if (limited) {
|
|
11088
|
+
io.err("mjolnir analyze: resource limit reached; cross-file analysis is partial");
|
|
11089
|
+
return 2;
|
|
11090
|
+
}
|
|
10857
11091
|
return 0;
|
|
10858
11092
|
} catch (e) {
|
|
10859
11093
|
internalErrorMessage(e, io.err, false);
|
|
@@ -10966,9 +11200,12 @@ function runCiAdapterCommand(argv, io) {
|
|
|
10966
11200
|
* file with inline CSS and JavaScript, suitable for embedding
|
|
10967
11201
|
* in team wikis or sharing via static hosting.
|
|
10968
11202
|
*/
|
|
11203
|
+
function escapeHtml(text) {
|
|
11204
|
+
return sanitizeErrorText(text, { maxLength: 1e3 }).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
11205
|
+
}
|
|
10969
11206
|
function generateDashboardHtml(data) {
|
|
10970
11207
|
const scoreColor = data.score === null ? "#888" : data.score >= 80 ? "#22c55e" : data.score >= 60 ? "#eab308" : "#ef4444";
|
|
10971
|
-
const rows = data.findings.map((f) => `<tr><td>${f.ruleId}</td><td><span style="color:${f.severity === "error" ? "#ef4444" : f.severity === "warning" ? "#eab308" : "#22c55e"}">${f.severity}</span></td><td>${f.file}</td><td>${f.message}</td></tr>`).join("");
|
|
11208
|
+
const rows = data.findings.map((f) => `<tr><td>${escapeHtml(f.ruleId)}</td><td><span style="color:${f.severity === "error" ? "#ef4444" : f.severity === "warning" ? "#eab308" : "#22c55e"}">${escapeHtml(f.severity)}</span></td><td>${escapeHtml(f.file)}</td><td>${escapeHtml(f.message)}</td></tr>`).join("");
|
|
10972
11209
|
return `<!DOCTYPE html>
|
|
10973
11210
|
<html lang="en">
|
|
10974
11211
|
<head>
|
|
@@ -10989,7 +11226,7 @@ function generateDashboardHtml(data) {
|
|
|
10989
11226
|
</head>
|
|
10990
11227
|
<body>
|
|
10991
11228
|
<h1>🔍 Quality Dashboard</h1>
|
|
10992
|
-
<p>Generated: ${data.generatedAt}</p>
|
|
11229
|
+
<p>Generated: ${escapeHtml(data.generatedAt)}</p>
|
|
10993
11230
|
<div class="score">${data.score !== null ? data.score + "/100" : "N/A"}</div>
|
|
10994
11231
|
<div class="kpi-grid">
|
|
10995
11232
|
<div class="kpi"><div class="value">${data.totalFindings}</div><div class="label">Findings</div></div>
|
|
@@ -11010,13 +11247,20 @@ async function runDashboardCommand(argv, io) {
|
|
|
11010
11247
|
io.err(`mjolnir dashboard: target does not exist: ${target}`);
|
|
11011
11248
|
return 10;
|
|
11012
11249
|
}
|
|
11250
|
+
const root = resolve(target);
|
|
11251
|
+
const resolvedOutput = resolve(outputPath);
|
|
11252
|
+
const outputRelative = relative(root, resolvedOutput);
|
|
11253
|
+
if (isAbsolute(outputRelative) || outputRelative.startsWith("..")) {
|
|
11254
|
+
io.err("mjolnir dashboard: output must stay within the target root");
|
|
11255
|
+
return 10;
|
|
11256
|
+
}
|
|
11013
11257
|
try {
|
|
11014
|
-
const { runScan } = await import("./scan-pipeline-
|
|
11258
|
+
const { runScan } = await import("./scan-pipeline-CRe7-MQn.mjs");
|
|
11015
11259
|
const result = await runScan({
|
|
11016
11260
|
target,
|
|
11017
11261
|
json: true,
|
|
11018
11262
|
verbose: false,
|
|
11019
|
-
maxDurationMs:
|
|
11263
|
+
maxDurationMs: 6e5,
|
|
11020
11264
|
scopeChanged: false,
|
|
11021
11265
|
format: "json",
|
|
11022
11266
|
strict: false
|
|
@@ -11036,8 +11280,8 @@ async function runDashboardCommand(argv, io) {
|
|
|
11036
11280
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11037
11281
|
};
|
|
11038
11282
|
const html = generateDashboardHtml(data);
|
|
11039
|
-
|
|
11040
|
-
io.out(`Dashboard written to ${
|
|
11283
|
+
writeFileAtomic(resolvedOutput, html, { encoding: "utf8" });
|
|
11284
|
+
io.out(`Dashboard written to ${resolvedOutput}`);
|
|
11041
11285
|
io.out(`Score: ${data.score !== null ? data.score + "/100" : "N/A"}`);
|
|
11042
11286
|
io.out(`Findings: ${data.totalFindings} (${data.errorCount} errors, ${data.warningCount} warnings)`);
|
|
11043
11287
|
return data.errorCount > 0 ? 1 : 0;
|
|
@@ -14281,7 +14525,7 @@ function loadPolicy(path) {
|
|
|
14281
14525
|
}
|
|
14282
14526
|
let record;
|
|
14283
14527
|
try {
|
|
14284
|
-
record = parseJsonFile(text, path, isRecord$
|
|
14528
|
+
record = parseJsonFile(text, path, isRecord$3);
|
|
14285
14529
|
} catch (error) {
|
|
14286
14530
|
throw new ConfigValidationError(`invalid suppression policy: ${path}`, { cause: error });
|
|
14287
14531
|
}
|
|
@@ -14300,7 +14544,7 @@ function loadPolicy(path) {
|
|
|
14300
14544
|
};
|
|
14301
14545
|
}
|
|
14302
14546
|
function isTrustSnapshot(value) {
|
|
14303
|
-
return isRecord$
|
|
14547
|
+
return isRecord$3(value) && typeof value.scanId === "string" && typeof value.timestamp === "string" && (typeof value.score === "number" || value.score === null) && typeof value.findings === "number" && typeof value.errors === "number" && typeof value.warnings === "number" && typeof value.infos === "number" && typeof value.advisory === "number" && typeof value.trustLevel === "string" && typeof value.confidence === "number" && typeof value.evidenceCoverage === "number" && typeof value.inconclusiveRate === "number" && typeof value.partial === "boolean" && typeof value.frameworkCount === "number" && typeof value.ruleCount === "number";
|
|
14304
14548
|
}
|
|
14305
14549
|
function loadTrustHistory(path) {
|
|
14306
14550
|
if (!existsSync(path)) return [];
|
|
@@ -14326,18 +14570,18 @@ function isStringArray(value) {
|
|
|
14326
14570
|
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
14327
14571
|
}
|
|
14328
14572
|
function isValidAnalysisStatus(value) {
|
|
14329
|
-
if (!isRecord$
|
|
14573
|
+
if (!isRecord$3(value)) return false;
|
|
14330
14574
|
return (value.discovery === "complete" || value.discovery === "partial") && (value.rules === "complete" || value.rules === "partial") && isNonNegativeInteger(value.skippedFiles) && isFiniteNumber(value.durationMs) && value.durationMs >= 0 && (value.truncationReasons === void 0 || isStringArray(value.truncationReasons)) && (value.rulesCrashed === void 0 || isNonNegativeInteger(value.rulesCrashed));
|
|
14331
14575
|
}
|
|
14332
14576
|
function isValidDimension(value) {
|
|
14333
|
-
return isRecord$
|
|
14577
|
+
return isRecord$3(value) && typeof value.category === "string" && isFiniteNumber(value.score) && value.score >= 0 && value.score <= 100 && isNonNegativeInteger(value.errors) && isNonNegativeInteger(value.warnings) && isNonNegativeInteger(value.infos);
|
|
14334
14578
|
}
|
|
14335
14579
|
function isValidFinding(value) {
|
|
14336
|
-
if (!isRecord$
|
|
14580
|
+
if (!isRecord$3(value)) return false;
|
|
14337
14581
|
return typeof value.ruleId === "string" && value.ruleId.length > 0 && typeof value.category === "string" && typeof value.file === "string" && value.file.length > 0 && isPositiveInteger(value.line) && isPositiveInteger(value.column) && typeof value.message === "string" && value.message.length > 0 && typeof value.severity === "string" && typeof value.confidence === "string" && typeof value.findingType === "string" && typeof value.qaImpact === "string" && typeof value.why === "string" && typeof value.fix === "string";
|
|
14338
14582
|
}
|
|
14339
14583
|
function isValidTrustSummary(value) {
|
|
14340
|
-
if (!isRecord$
|
|
14584
|
+
if (!isRecord$3(value)) return false;
|
|
14341
14585
|
return [
|
|
14342
14586
|
"L0",
|
|
14343
14587
|
"L1",
|
|
@@ -14348,31 +14592,31 @@ function isValidTrustSummary(value) {
|
|
|
14348
14592
|
].includes(String(value.level)) && isFiniteNumber(value.confidence) && value.confidence >= 0 && value.confidence <= 1 && isFiniteNumber(value.evidenceCoverage) && value.evidenceCoverage >= 0 && value.evidenceCoverage <= 1 && isFiniteNumber(value.inconclusiveRate) && value.inconclusiveRate >= 0 && value.inconclusiveRate <= 1 && (value.measuredFpOfFiredRules === void 0 || isFiniteNumber(value.measuredFpOfFiredRules) && value.measuredFpOfFiredRules >= 0 && value.measuredFpOfFiredRules <= 1) && isStringArray(value.provisionalRuleIds) && (value.confidenceCeiling === void 0 || isFiniteNumber(value.confidenceCeiling) && value.confidenceCeiling >= 0 && value.confidenceCeiling <= 1) && isStringArray(value.ceilingReasons);
|
|
14349
14593
|
}
|
|
14350
14594
|
function isValidProvenance(value) {
|
|
14351
|
-
if (!isRecord$
|
|
14595
|
+
if (!isRecord$3(value)) return false;
|
|
14352
14596
|
return isNonNegativeInteger(value.testFiles) && isNonNegativeInteger(value.generatedMarkedFiles) && isNonNegativeInteger(value.codegenLikeFiles) && isFiniteNumber(value.shareMarkedGenerated) && value.shareMarkedGenerated >= 0 && value.shareMarkedGenerated <= 1 && isNonNegativeInteger(value.findingsInGeneratedFiles) && isNonNegativeInteger(value.findingsInUnmarkedFiles) && typeof value.note === "string";
|
|
14353
14597
|
}
|
|
14354
14598
|
function isValidForensicVerdicts(value) {
|
|
14355
|
-
if (!isRecord$
|
|
14599
|
+
if (!isRecord$3(value) || !isRecord$3(value.byVerdict)) return false;
|
|
14356
14600
|
return isNonNegativeInteger(value.classifications) && isNonNegativeInteger(value.inconclusive) && Object.values(value.byVerdict).every((count) => isNonNegativeInteger(count));
|
|
14357
14601
|
}
|
|
14358
14602
|
function isValidSummary(value) {
|
|
14359
|
-
if (!isRecord$
|
|
14603
|
+
if (!isRecord$3(value)) return false;
|
|
14360
14604
|
return typeof value.digest === "string" && value.digest.length > 0 && isNonNegativeInteger(value.findings) && (value.score === null || isFiniteNumber(value.score) && value.score >= 0 && value.score <= 100) && isNonNegativeInteger(value.errors) && isNonNegativeInteger(value.warnings) && isNonNegativeInteger(value.infos) && isNonNegativeInteger(value.advisory);
|
|
14361
14605
|
}
|
|
14362
14606
|
function isValidAnnotation(value) {
|
|
14363
|
-
if (!isRecord$
|
|
14607
|
+
if (!isRecord$3(value)) return false;
|
|
14364
14608
|
return typeof value.path === "string" && value.path.length > 0 && isPositiveInteger(value.start_line) && (value.annotation_level === "failure" || value.annotation_level === "warning" || value.annotation_level === "notice") && typeof value.message === "string" && typeof value.ruleId === "string" && value.ruleId.length > 0 && (value.detectorRevision === void 0 || isPositiveInteger(value.detectorRevision)) && typeof value.advisory === "boolean";
|
|
14365
14609
|
}
|
|
14366
14610
|
function isValidCompleteness(value) {
|
|
14367
|
-
if (!isRecord$
|
|
14611
|
+
if (!isRecord$3(value)) return false;
|
|
14368
14612
|
return typeof value.partial === "boolean" && (value.discovery === "complete" || value.discovery === "partial") && (value.rules === "complete" || value.rules === "partial") && isNonNegativeInteger(value.skippedFiles) && isNonNegativeInteger(value.rulesCrashed) && isStringArray(value.truncationReasons) && typeof value.frameworkDetectionUnknown === "boolean" && isFiniteNumber(value.durationMs) && value.durationMs >= 0;
|
|
14369
14613
|
}
|
|
14370
14614
|
function isValidContract(value) {
|
|
14371
|
-
if (!isRecord$
|
|
14615
|
+
if (!isRecord$3(value)) return false;
|
|
14372
14616
|
return Number.isSafeInteger(value.contractVersion) && isValidSummary(value.summary) && Array.isArray(value.annotations) && value.annotations.length <= 50 && value.annotations.every((annotation) => isValidAnnotation(annotation)) && typeof value.annotationsTruncated === "boolean" && isValidCompleteness(value.completeness) && (value.trustSummary === void 0 || isValidTrustSummary(value.trustSummary)) && (value.provenance === void 0 || isValidProvenance(value.provenance)) && (value.forensicVerdicts === void 0 || isValidForensicVerdicts(value.forensicVerdicts));
|
|
14373
14617
|
}
|
|
14374
14618
|
function isScanResultDocument(value) {
|
|
14375
|
-
if (!isRecord$
|
|
14619
|
+
if (!isRecord$3(value) || value.schemaVersion !== 1) return false;
|
|
14376
14620
|
return (value.score === null || isFiniteNumber(value.score) && value.score >= 0 && value.score <= 100) && typeof value.partial === "boolean" && isStringArray(value.frameworks) && Array.isArray(value.dimensions) && value.dimensions.every((dimension) => isValidDimension(dimension)) && Array.isArray(value.findings) && value.findings.every((finding) => isValidFinding(finding)) && isValidAnalysisStatus(value.analysisStatus) && (value.agenticProfile === void 0 || isValidProvenance(value.agenticProfile)) && (value.trustSummary === void 0 || isValidTrustSummary(value.trustSummary)) && (value.forensicVerdicts === void 0 || isValidForensicVerdicts(value.forensicVerdicts)) && isValidContract(value.contract);
|
|
14377
14621
|
}
|
|
14378
14622
|
function emitVerification(verification, json, io) {
|
|
@@ -14628,12 +14872,14 @@ async function runCIIntegrityCommand(argv, io = {
|
|
|
14628
14872
|
* keeps every existing consumer stable.
|
|
14629
14873
|
*/
|
|
14630
14874
|
const { runScan, buildUniversalRules, fallbackWorkspace, pathMatchesGlob, isValidFindingRecord, discoverAndParseRuntimeReport, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, EVIDENCE_OVERRIDES, SUITE_INVALIDATING_RULE_IDS } = scan_pipeline_exports;
|
|
14875
|
+
const DEFAULT_MAX_DURATION_MS = 6e5;
|
|
14876
|
+
const MAX_DURATION_MS = 36e5;
|
|
14631
14877
|
function parseArgs(argv, onError) {
|
|
14632
14878
|
const args = {
|
|
14633
14879
|
target: ".",
|
|
14634
14880
|
json: false,
|
|
14635
14881
|
verbose: false,
|
|
14636
|
-
maxDurationMs:
|
|
14882
|
+
maxDurationMs: DEFAULT_MAX_DURATION_MS,
|
|
14637
14883
|
scopeChanged: false,
|
|
14638
14884
|
format: "terminal"
|
|
14639
14885
|
};
|
|
@@ -14679,7 +14925,7 @@ function parseArgs(argv, onError) {
|
|
|
14679
14925
|
args.base = ref;
|
|
14680
14926
|
} else if (a === "--max-duration") {
|
|
14681
14927
|
const v = Number(argv[++i]);
|
|
14682
|
-
if (!Number.isFinite(v) || v <= 0) return reject({
|
|
14928
|
+
if (!Number.isFinite(v) || v <= 0 || v * 1e3 > 36e5) return reject({
|
|
14683
14929
|
flag: "--max-duration",
|
|
14684
14930
|
token: argv[i]
|
|
14685
14931
|
});
|
|
@@ -15047,4 +15293,4 @@ if (isEntryPoint()) try {
|
|
|
15047
15293
|
process$1.exitCode = 20;
|
|
15048
15294
|
}
|
|
15049
15295
|
//#endregion
|
|
15050
|
-
export { ENGINE_VERSION as CLI_VERSION, EVIDENCE_OVERRIDES, KNOWN_RULE_IDS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverAndParseRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, parseArgsOrUsage, pathMatchesGlob, printUsage, renderScanOutput, runAnalyzeCommand, runBadgeCommand, runBaselineCommand, runBusinessCaseCommand, runCIIntegrityCommand, runCiAdapterCommand, runCiInstall, runContractVerifyCommand, runCreateRuleCommand, runCrossFileCommand, runDashboardCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runEnterpriseCommand, runEvidenceGraphCommand, runExecReportCommand, runExplainCommand, runFixCommand, runForensicsCommand, runFrameworkMaturityCommand, runHandoffCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runInstallCommand, runMaturityCommand, runMutationCommand, runPolicyCommand, runPrCommentCommand, runPwReportCommand, runQuarantineCommand, runReleaseReportCommand, runReleaseTrustCommand, runReportPlaywrightCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSummaryCommand, runSuppressionGateCommand, runSuppressions, runTrendCommand, runTriageCommand, runTrustReportCommand, runTrustTrendCommand, runVerifyCommand, runWhyCommand, usageErrorMessage, validateScanTarget };
|
|
15296
|
+
export { ENGINE_VERSION as CLI_VERSION, DEFAULT_MAX_DURATION_MS, EVIDENCE_OVERRIDES, KNOWN_RULE_IDS, MAX_DURATION_MS, OVERLAP_META_BY_RULE_ID, SUITE_INVALIDATING_RULE_IDS, buildUniversalRules, discoverAndParseRuntimeReport, err, exitForFindings, fallbackWorkspace, internalErrorMessage, isEntryPoint, isValidFindingRecord, levenshtein, main, nearestFlags, out, parseArgs, parseArgsOrUsage, pathMatchesGlob, printUsage, renderScanOutput, runAnalyzeCommand, runBadgeCommand, runBaselineCommand, runBusinessCaseCommand, runCIIntegrityCommand, runCiAdapterCommand, runCiInstall, runContractVerifyCommand, runCreateRuleCommand, runCrossFileCommand, runDashboardCommand, runDebtCommand, runDiffCommand, runDoctorCommand, runDoctorPlaywright, runEnterpriseCommand, runEvidenceGraphCommand, runExecReportCommand, runExplainCommand, runFixCommand, runForensicsCommand, runFrameworkMaturityCommand, runHandoffCommand, runHandoverCommand, runHelpCommand, runImpactCommand, runInitCommand, runInstallCommand, runMaturityCommand, runMutationCommand, runPolicyCommand, runPrCommentCommand, runPwReportCommand, runQuarantineCommand, runReleaseReportCommand, runReleaseTrustCommand, runReportPlaywrightCommand, runRulesCommand, runScan, runScanCommand, runStatsCommand, runSummaryCommand, runSuppressionGateCommand, runSuppressions, runTrendCommand, runTriageCommand, runTrustReportCommand, runTrustTrendCommand, runVerifyCommand, runWhyCommand, usageErrorMessage, validateScanTarget };
|