pqcheck 0.16.19 → 0.16.21
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/README.md +1 -1
- package/bin/pqcheck.js +102 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://www.npmjs.com/package/pqcheck)
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
|
|
11
|
-
> **Latest: v0.16.
|
|
11
|
+
> **Latest: v0.16.21** — `ship_decision` now folds posture into the headline routing decision (worst-of drift + posture) — closes a footgun where a clean-drift F-posture deploy would have shipped under the original "`pass` → announce" protocol. Posture rubric recalibrated for partial credit: a site with valid HSTS preload + everything else missing now scores ~29 / grade D, not 0 / F. Fix snippets now emit inline in a `CIPHERWAKE_POSTURE_FIXES` block — no JSON round-trip needed to read them. Empty legacy `grade=` field dropped from the AI block. [Full changelog →](./CHANGELOG.md)
|
|
12
12
|
|
|
13
13
|
## Two ways to use it
|
|
14
14
|
|
package/bin/pqcheck.js
CHANGED
|
@@ -495,21 +495,46 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
495
495
|
}
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
+
// R86 — surface absolute posture grade + remediation alongside drift verdict.
|
|
499
|
+
// The grade= field carries DBR grade; posture_grade= is the new absolute
|
|
500
|
+
// posture (A+/A/B/C/D/F based on header presence/strength).
|
|
501
|
+
// posture_decision separates the absolute-state routing signal from the
|
|
502
|
+
// drift-based ship_decision so a weak-but-unchanged site gets a review
|
|
503
|
+
// nudge distinct from drift.
|
|
504
|
+
const posture = report.posture || null;
|
|
505
|
+
const postureFields = posture ? {
|
|
506
|
+
posture_grade: posture.grade,
|
|
507
|
+
posture_score: posture.score,
|
|
508
|
+
posture_decision: posture.decision,
|
|
509
|
+
posture_missing: (posture.missing || []).join(",") || "none",
|
|
510
|
+
posture_leaks: (posture.info_leaks || []).join("; ") || "none",
|
|
511
|
+
posture_findings_count: (posture.findings || []).length,
|
|
512
|
+
posture_fixes_count: (posture.fixes || []).length,
|
|
513
|
+
} : {};
|
|
514
|
+
const effectiveShip = combineShipDecision(shipDecision, posture?.decision);
|
|
498
515
|
console.log(formatAiFooterBlock({
|
|
499
|
-
status:
|
|
516
|
+
status: effectiveShip,
|
|
500
517
|
domain,
|
|
501
518
|
kind: "scan",
|
|
502
|
-
dbr: typeof report.score === "number" ? report.score.toFixed(1) :
|
|
503
|
-
grade: report.grade ||
|
|
519
|
+
dbr: typeof report.score === "number" ? report.score.toFixed(1) : undefined,
|
|
520
|
+
grade: report.grade || undefined,
|
|
504
521
|
max_severity: maxSev,
|
|
505
|
-
ship_decision:
|
|
522
|
+
ship_decision: effectiveShip,
|
|
523
|
+
ship_decision_drift: shipDecision,
|
|
524
|
+
ship_decision_posture: posture?.decision,
|
|
506
525
|
unreachable: unreachable ? "true" : "false",
|
|
507
526
|
top_issue: topFinding?.id || topFinding?.title || "none",
|
|
508
527
|
findings_high: findings.filter((f) => severityRank(f.severity) === 3).length,
|
|
509
528
|
findings_critical: findings.filter((f) => severityRank(f.severity) === 4).length,
|
|
510
529
|
scanned_at: new Date().toISOString(),
|
|
511
530
|
advisory_only: "true",
|
|
531
|
+
scope: "trust_surface_drift_plus_absolute_posture",
|
|
532
|
+
scope_note: "ship_decision = worst-of(drift, absolute posture). pass means BOTH no drift AND posture grade A+/A. ship_decision_drift and ship_decision_posture expose the two inputs separately. Cipherwake does NOT verify app functionality — pair with Playwright e2e for full deploy safety.",
|
|
533
|
+
...postureFields,
|
|
512
534
|
}));
|
|
535
|
+
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
|
536
|
+
console.log(formatPostureFixesBlock(posture.fixes));
|
|
537
|
+
}
|
|
513
538
|
console.log("");
|
|
514
539
|
|
|
515
540
|
// Threshold check still applies under --ai (script-pipeable). Otherwise
|
|
@@ -517,7 +542,7 @@ async function runOneScan({ domain, format, quiet, threshold, webhookUrl, multi,
|
|
|
517
542
|
if (threshold !== null && typeof report.score === "number" && report.score >= threshold) {
|
|
518
543
|
return 2;
|
|
519
544
|
}
|
|
520
|
-
return shipDecisionExitCode(
|
|
545
|
+
return shipDecisionExitCode(effectiveShip);
|
|
521
546
|
}
|
|
522
547
|
|
|
523
548
|
// Output dispatch
|
|
@@ -732,6 +757,46 @@ function computeShipDecision({ maxSeverity, hasUnexpectedDiff, scoreDelta }) {
|
|
|
732
757
|
return "pass";
|
|
733
758
|
}
|
|
734
759
|
|
|
760
|
+
// R86.2 (2026-06-03) — fold absolute posture grade into ship_decision so an
|
|
761
|
+
// AI agent following the original "ship_decision=pass → safe to announce"
|
|
762
|
+
// protocol does NOT ship a site with F-grade posture just because nothing
|
|
763
|
+
// drifted since last scan. The headline decision must reflect both drift
|
|
764
|
+
// and absolute state.
|
|
765
|
+
//
|
|
766
|
+
// Order: block > review > pass. Worst-of-both wins.
|
|
767
|
+
const SHIP_DECISION_RANK = { pass: 0, review: 1, block: 2 };
|
|
768
|
+
function combineShipDecision(driftDecision, postureDecision) {
|
|
769
|
+
if (!postureDecision) return driftDecision;
|
|
770
|
+
const a = SHIP_DECISION_RANK[driftDecision] ?? 0;
|
|
771
|
+
const b = SHIP_DECISION_RANK[postureDecision] ?? 0;
|
|
772
|
+
return a >= b ? driftDecision : postureDecision;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// R86.3 (2026-06-03) — emit per-finding fix snippets in a separate parseable
|
|
776
|
+
// block after the AI guard block. The guard block stays compact key=value; the
|
|
777
|
+
// fixes are multi-line code so they get their own block with explicit
|
|
778
|
+
// start/end markers + per-fix delimiters. Agents can parse the fixes
|
|
779
|
+
// independently and apply them without round-tripping to the JSON response.
|
|
780
|
+
function formatPostureFixesBlock(fixes) {
|
|
781
|
+
if (!Array.isArray(fixes) || fixes.length === 0) return "";
|
|
782
|
+
const lines = ["", "CIPHERWAKE_POSTURE_FIXES"];
|
|
783
|
+
for (let i = 0; i < fixes.length; i++) {
|
|
784
|
+
const f = fixes[i] || {};
|
|
785
|
+
lines.push(`--- FIX ${i + 1} ---`);
|
|
786
|
+
lines.push(`finding_id=${String(f.finding_id || "").replace(/[\r\n]+/g, " ")}`);
|
|
787
|
+
lines.push(`title=${String(f.title || "").replace(/[\r\n]+/g, " ")}`);
|
|
788
|
+
lines.push(`framework=${String(f.framework || "").replace(/[\r\n]+/g, " ")}`);
|
|
789
|
+
lines.push(`file_target=${String(f.file_target || "").replace(/[\r\n]+/g, " ")}`);
|
|
790
|
+
lines.push("snippet:");
|
|
791
|
+
const snippet = String(f.snippet || "");
|
|
792
|
+
for (const line of snippet.split(/\r?\n/)) {
|
|
793
|
+
lines.push(line);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
lines.push("END_CIPHERWAKE_POSTURE_FIXES");
|
|
797
|
+
return color("dim", lines.join("\n"));
|
|
798
|
+
}
|
|
799
|
+
|
|
735
800
|
function aiBannerColor(shipDecision) {
|
|
736
801
|
if (shipDecision === "pass") return "green";
|
|
737
802
|
if (shipDecision === "block") return "red";
|
|
@@ -3400,24 +3465,47 @@ async function runTrustDiffCommand(args) {
|
|
|
3400
3465
|
console.log(color("dim", " Run with --verbose to see all verified signals."));
|
|
3401
3466
|
}
|
|
3402
3467
|
|
|
3468
|
+
// R86 — posture grade + remediation alongside drift. trust-diff API returns
|
|
3469
|
+
// result.current_report which contains the full scan; if so, read posture
|
|
3470
|
+
// from it. Otherwise gracefully omit (older API responses without posture).
|
|
3471
|
+
const currentReport = result.current_report || result.report || null;
|
|
3472
|
+
const posture = currentReport?.posture || null;
|
|
3473
|
+
const postureFields = posture ? {
|
|
3474
|
+
posture_grade: posture.grade,
|
|
3475
|
+
posture_score: posture.score,
|
|
3476
|
+
posture_decision: posture.decision,
|
|
3477
|
+
posture_missing: (posture.missing || []).join(",") || "none",
|
|
3478
|
+
posture_leaks: (posture.info_leaks || []).join("; ") || "none",
|
|
3479
|
+
posture_findings_count: (posture.findings || []).length,
|
|
3480
|
+
posture_fixes_count: (posture.fixes || []).length,
|
|
3481
|
+
} : {};
|
|
3482
|
+
const effectiveShip = combineShipDecision(shipDecision, posture?.decision);
|
|
3403
3483
|
console.log(formatAiFooterBlock({
|
|
3404
|
-
status:
|
|
3484
|
+
status: effectiveShip,
|
|
3405
3485
|
domain,
|
|
3406
3486
|
kind: "trust-diff",
|
|
3407
3487
|
baseline,
|
|
3408
3488
|
verdict,
|
|
3409
3489
|
delta_count: deltas.length,
|
|
3410
3490
|
max_severity: maxSev,
|
|
3411
|
-
ship_decision:
|
|
3491
|
+
ship_decision: effectiveShip,
|
|
3492
|
+
ship_decision_drift: shipDecision,
|
|
3493
|
+
ship_decision_posture: posture?.decision,
|
|
3412
3494
|
top_issue: topDelta?.id || topDelta?.type || "none",
|
|
3413
|
-
top_issue_title: topDelta?.title ||
|
|
3414
|
-
dbr: typeof result.current_score === "number" ? result.current_score.toFixed(1) :
|
|
3415
|
-
grade: result.current_grade ||
|
|
3416
|
-
quota_used: result.quota?.used_this_month ??
|
|
3417
|
-
quota_limit: result.quota?.monthly_limit ??
|
|
3495
|
+
top_issue_title: topDelta?.title || undefined,
|
|
3496
|
+
dbr: typeof result.current_score === "number" ? result.current_score.toFixed(1) : undefined,
|
|
3497
|
+
grade: result.current_grade || undefined,
|
|
3498
|
+
quota_used: result.quota?.used_this_month ?? undefined,
|
|
3499
|
+
quota_limit: result.quota?.monthly_limit ?? undefined,
|
|
3418
3500
|
scanned_at: new Date().toISOString(),
|
|
3419
3501
|
advisory_only: "true",
|
|
3502
|
+
scope: "trust_surface_drift_plus_absolute_posture",
|
|
3503
|
+
scope_note: "ship_decision = worst-of(drift, absolute posture). pass means BOTH no drift AND posture grade A+/A. ship_decision_drift and ship_decision_posture expose the two inputs separately. Cipherwake does NOT verify app functionality — pair with Playwright e2e for full deploy safety.",
|
|
3504
|
+
...postureFields,
|
|
3420
3505
|
}));
|
|
3506
|
+
if (posture && Array.isArray(posture.fixes) && posture.fixes.length > 0) {
|
|
3507
|
+
console.log(formatPostureFixesBlock(posture.fixes));
|
|
3508
|
+
}
|
|
3421
3509
|
console.log("");
|
|
3422
3510
|
|
|
3423
3511
|
await writeLastScanFile({
|
|
@@ -3426,13 +3514,13 @@ async function runTrustDiffCommand(args) {
|
|
|
3426
3514
|
score: typeof result.current_score === "number" ? result.current_score : null,
|
|
3427
3515
|
grade: result.current_grade || null,
|
|
3428
3516
|
max_severity: maxSev,
|
|
3429
|
-
ship_decision:
|
|
3517
|
+
ship_decision: effectiveShip,
|
|
3430
3518
|
baseline,
|
|
3431
3519
|
delta_count: deltas.length,
|
|
3432
3520
|
top_issue: topDelta?.id || topDelta?.title || null,
|
|
3433
3521
|
});
|
|
3434
3522
|
|
|
3435
|
-
process.exit(shipDecisionExitCode(
|
|
3523
|
+
process.exit(shipDecisionExitCode(effectiveShip));
|
|
3436
3524
|
}
|
|
3437
3525
|
|
|
3438
3526
|
// Format output
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.21",
|
|
4
4
|
"description": "Deploy gate for AI-coded web apps. `pqcheck deploy-check --ai` returns ship_decision=pass|review|block for Claude Code / Cursor / Copilot / Aider to gate deploys before they ship. Anonymous, no signup, free for first use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-coder",
|