pqcheck 0.16.22 → 0.16.23
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 +128 -3
- 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.23** — Closes the silent-no-op flag class. `--strict` now aliases `--strict-posture` in scan / deploy-check / trust-diff (audit caught the typo silently degrading to drift-only). And unknown flags now reject loudly with a closest-match suggestion + non-zero exit — `--stict-posture` (typo) → `error: did you mean --strict-posture?` instead of silently no-op'ing. Same principle applied to ourselves that Cipherwake exists to enforce for customers: a security tool can never silently proceed with weaker behaviour on an unrecognized signal. [Full changelog →](./CHANGELOG.md)
|
|
12
12
|
|
|
13
13
|
## Two ways to use it
|
|
14
14
|
|
package/bin/pqcheck.js
CHANGED
|
@@ -248,6 +248,14 @@ async function main() {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
// R86.7 — reject unknown flags loudly. Catches typo'd safety-critical
|
|
252
|
+
// flags like `--strict` (was intended --strict-posture) that previously
|
|
253
|
+
// silently no-op'd, leaving customers with a false sense of security.
|
|
254
|
+
const unknown = assertKnownFlags(args, "pqcheck <domain>");
|
|
255
|
+
if (unknown) {
|
|
256
|
+
process.exit(3);
|
|
257
|
+
}
|
|
258
|
+
|
|
251
259
|
const positional = args.filter((a) => !a.startsWith("-") && !isFlagValue(args, a));
|
|
252
260
|
const domains = [...positional, ...fileDomains]
|
|
253
261
|
.map((a) => normalizeDomain(a))
|
|
@@ -295,7 +303,13 @@ async function main() {
|
|
|
295
303
|
// absolute posture grade into ship_decision. Recommended ONLY after a
|
|
296
304
|
// site reaches A/B posture — opting in earlier produces cry-wolf gating
|
|
297
305
|
// on every deploy since most AI-coded sites grade D/F out of the box.
|
|
298
|
-
|
|
306
|
+
// Accept both `--strict-posture` (explicit) and `--strict` (short alias).
|
|
307
|
+
// Audit caught a footgun where customers typed `--strict` expecting the
|
|
308
|
+
// posture gate to fire and got silent drift-only mode instead. The
|
|
309
|
+
// `onboard` subcommand uses `--strict` for a different purpose (gate exit
|
|
310
|
+
// code on step failure) but that's a separate command — the alias is
|
|
311
|
+
// scoped to scan / deploy-check / trust-diff only.
|
|
312
|
+
const strictPosture = args.includes("--strict-posture") || args.includes("--strict");
|
|
299
313
|
|
|
300
314
|
// One-shot scan(s)
|
|
301
315
|
let worstExit = 0;
|
|
@@ -747,6 +761,100 @@ function parseAiMode(args) {
|
|
|
747
761
|
return args.includes("--ai") || args.includes("--agent");
|
|
748
762
|
}
|
|
749
763
|
|
|
764
|
+
// R86.7 (2026-06-04) — close the "silent no-op on unrecognized flag" class
|
|
765
|
+
// of footguns. Audit caught --strict (intended --strict-posture) silently
|
|
766
|
+
// degrading to drift-only mode without warning — the user believed they had
|
|
767
|
+
// the hard posture gate on. Same family as false-green pins: looks like it's
|
|
768
|
+
// doing something, silently isn't. The fix: validate every --foo token in
|
|
769
|
+
// args against a known-flags whitelist and reject loudly on unknown flags
|
|
770
|
+
// with a closest-match suggestion. For a security gate, "unknown flag →
|
|
771
|
+
// silently proceed with weaker behaviour" must never happen.
|
|
772
|
+
//
|
|
773
|
+
// Scope: universal whitelist applied at scan / deploy-check / trust-diff /
|
|
774
|
+
// preview-diff entry points. False acceptance of cross-command flags (e.g.
|
|
775
|
+
// passing --preview to deploy-check) is the SAME failure mode as today
|
|
776
|
+
// (silent ignore) — typo detection is the win. Per-command whitelists
|
|
777
|
+
// would be more correct but the universal list covers the audit footgun.
|
|
778
|
+
const KNOWN_FLAGS = new Set([
|
|
779
|
+
// Global / output mode
|
|
780
|
+
"--ai", "--agent", "--verbose", "--quiet", "--help", "--version", "--debug-network",
|
|
781
|
+
// Multi-domain
|
|
782
|
+
"--file", "--watch",
|
|
783
|
+
// Scan behaviour
|
|
784
|
+
"--fresh", "--force", "--threshold", "--webhook", "--json", "--csv", "--markdown",
|
|
785
|
+
"--sarif", "--gh-action", "--multi", "--lock", "--explain", "--plan", "--stdout",
|
|
786
|
+
// Posture gating (the audit catch)
|
|
787
|
+
"--strict", "--strict-posture",
|
|
788
|
+
// Format / output format
|
|
789
|
+
"--format",
|
|
790
|
+
// Trust-diff / deploy-check / preview-diff
|
|
791
|
+
"--baseline", "--fail-on", "--fail-on-new", "--guards", "--compare-transport",
|
|
792
|
+
"--write-baseline", "--preview", "--production",
|
|
793
|
+
// Setup / onboard / install consent
|
|
794
|
+
"--auto", "--manual", "--yes", "--no-open", "--domain", "--invoked-by",
|
|
795
|
+
"--consent-phrase", "--scope",
|
|
796
|
+
"--skip-checklist", "--skip-hook", "--skip-protocol", "--skip-scan",
|
|
797
|
+
"--skip-statusline", "--skip-vendors", "--skip-vscode", "--skip-workflow",
|
|
798
|
+
]);
|
|
799
|
+
|
|
800
|
+
function levenshtein(a, b) {
|
|
801
|
+
if (a === b) return 0;
|
|
802
|
+
const m = a.length, n = b.length;
|
|
803
|
+
if (m === 0) return n;
|
|
804
|
+
if (n === 0) return m;
|
|
805
|
+
const row = new Array(m + 1);
|
|
806
|
+
for (let i = 0; i <= m; i++) row[i] = i;
|
|
807
|
+
for (let j = 1; j <= n; j++) {
|
|
808
|
+
let prev = row[0];
|
|
809
|
+
row[0] = j;
|
|
810
|
+
for (let i = 1; i <= m; i++) {
|
|
811
|
+
const tmp = row[i];
|
|
812
|
+
row[i] = a[i - 1] === b[j - 1]
|
|
813
|
+
? prev
|
|
814
|
+
: 1 + Math.min(prev, row[i], row[i - 1]);
|
|
815
|
+
prev = tmp;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return row[m];
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function suggestFlag(unknown) {
|
|
822
|
+
let best = null, bestDist = Infinity;
|
|
823
|
+
for (const known of KNOWN_FLAGS) {
|
|
824
|
+
const d = levenshtein(unknown, known);
|
|
825
|
+
if (d < bestDist) { bestDist = d; best = known; }
|
|
826
|
+
}
|
|
827
|
+
// Only suggest if reasonably close (≤ 3 edits or ≤ half the unknown's length)
|
|
828
|
+
const threshold = Math.max(3, Math.floor(unknown.length / 2));
|
|
829
|
+
return bestDist <= threshold ? best : null;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function assertKnownFlags(args, cmdName) {
|
|
833
|
+
const unknown = [];
|
|
834
|
+
for (const tok of args) {
|
|
835
|
+
if (typeof tok !== "string" || !tok.startsWith("--") || tok === "--") continue;
|
|
836
|
+
// Strip `--foo=bar` → `--foo` so equals-value forms validate against the bare flag
|
|
837
|
+
const flag = tok.includes("=") ? tok.slice(0, tok.indexOf("=")) : tok;
|
|
838
|
+
if (!KNOWN_FLAGS.has(flag)) unknown.push(flag);
|
|
839
|
+
}
|
|
840
|
+
if (unknown.length === 0) return null;
|
|
841
|
+
// Emit error to stderr with closest-match suggestion, return non-null
|
|
842
|
+
// sentinel so the caller can exit non-zero. We do NOT process.exit here
|
|
843
|
+
// because the AI-mode caller needs to emit a structured guard block
|
|
844
|
+
// before exiting (so AI agents parsing the block don't see a missing
|
|
845
|
+
// CIPHERWAKE_AI_GUARD_RESULT and fall through to a different error path).
|
|
846
|
+
for (const u of unknown) {
|
|
847
|
+
const sug = suggestFlag(u);
|
|
848
|
+
process.stderr.write(color("red", `error: unknown flag ${u} for ${cmdName}\n`));
|
|
849
|
+
if (sug) {
|
|
850
|
+
process.stderr.write(color("yellow", ` did you mean ${sug}?\n`));
|
|
851
|
+
} else {
|
|
852
|
+
process.stderr.write(color("dim", ` run \`npx pqcheck --help\` for the flag list.\n`));
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
return unknown;
|
|
856
|
+
}
|
|
857
|
+
|
|
750
858
|
function severityRank(s) {
|
|
751
859
|
const map = { critical: 4, high: 3, medium: 2, low: 1, info: 0, none: -1 };
|
|
752
860
|
return map[String(s || "none").toLowerCase()] ?? 0;
|
|
@@ -3365,10 +3473,16 @@ async function runScanBasedDeployCheck(domain, args) {
|
|
|
3365
3473
|
}
|
|
3366
3474
|
|
|
3367
3475
|
async function runTrustDiffCommand(args) {
|
|
3476
|
+
// R86.7 — reject unknown flags before any other parsing. Same rationale
|
|
3477
|
+
// as the bare-scan path: typo'd safety-critical flags must fail loud,
|
|
3478
|
+
// not silently degrade.
|
|
3479
|
+
if (assertKnownFlags(args, "pqcheck trust-diff")) {
|
|
3480
|
+
process.exit(3);
|
|
3481
|
+
}
|
|
3368
3482
|
const positional = args.filter((a) => !a.startsWith("-") && !isFlagValue(args, a));
|
|
3369
3483
|
if (positional.length === 0) {
|
|
3370
3484
|
console.error(color("red", "error: pqcheck trust-diff requires a domain"));
|
|
3371
|
-
console.error(color("dim", "Usage: npx pqcheck trust-diff <domain> [--baseline last-week] [--fail-on high] [--format pretty|json|sarif|github]"));
|
|
3485
|
+
console.error(color("dim", "Usage: npx pqcheck trust-diff <domain> [--baseline last-week] [--fail-on high] [--format pretty|json|sarif|github] [--strict-posture]"));
|
|
3372
3486
|
process.exit(3);
|
|
3373
3487
|
}
|
|
3374
3488
|
const domain = normalizeDomain(positional[0]);
|
|
@@ -3384,7 +3498,13 @@ async function runTrustDiffCommand(args) {
|
|
|
3384
3498
|
// is drift-only (per-deploy regression gate); --strict-posture opts into
|
|
3385
3499
|
// worst-of(drift, posture). Recommended only after a site reaches A/B
|
|
3386
3500
|
// posture to lock that in.
|
|
3387
|
-
|
|
3501
|
+
// Accept both `--strict-posture` (explicit) and `--strict` (short alias).
|
|
3502
|
+
// Audit caught a footgun where customers typed `--strict` expecting the
|
|
3503
|
+
// posture gate to fire and got silent drift-only mode instead. The
|
|
3504
|
+
// `onboard` subcommand uses `--strict` for a different purpose (gate exit
|
|
3505
|
+
// code on step failure) but that's a separate command — the alias is
|
|
3506
|
+
// scoped to scan / deploy-check / trust-diff only.
|
|
3507
|
+
const strictPosture = args.includes("--strict-posture") || args.includes("--strict");
|
|
3388
3508
|
|
|
3389
3509
|
// Build headers conditionally — Authorization is set ONLY if the user has
|
|
3390
3510
|
// an API key. Without it, the server's applyRepoQuota falls through to the
|
|
@@ -3881,6 +4001,11 @@ async function runGuardsRunCommand(args) {
|
|
|
3881
4001
|
}
|
|
3882
4002
|
|
|
3883
4003
|
async function runPreviewDiffCommand(args) {
|
|
4004
|
+
// R86.7 — reject unknown flags before parsing. Closes the silent-no-op
|
|
4005
|
+
// class of footguns for the preview-diff command too.
|
|
4006
|
+
if (assertKnownFlags(args, "pqcheck preview-diff")) {
|
|
4007
|
+
process.exit(3);
|
|
4008
|
+
}
|
|
3884
4009
|
const previewUrl = parseFlag(args, "--preview");
|
|
3885
4010
|
const productionUrl = parseFlag(args, "--production");
|
|
3886
4011
|
if (!previewUrl || !productionUrl) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pqcheck",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.23",
|
|
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",
|