@pretense/cli 0.6.15 → 0.6.16
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 +21 -6
- package/dist/{chunk-AULCU5QN.js → chunk-MH6CXHTM.js} +1 -1
- package/dist/index.js +1439 -1018
- package/dist/postinstall.js +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
PRETENSE_VERSION
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-MH6CXHTM.js";
|
|
5
5
|
import {
|
|
6
6
|
__commonJS,
|
|
7
7
|
__toESM,
|
|
@@ -221,8 +221,8 @@ var require_brace_expansion = __commonJS({
|
|
|
221
221
|
|
|
222
222
|
// src/index.ts
|
|
223
223
|
init_esm_shims();
|
|
224
|
-
import { Command as
|
|
225
|
-
import
|
|
224
|
+
import { Command as Command23 } from "commander";
|
|
225
|
+
import chalk23 from "chalk";
|
|
226
226
|
|
|
227
227
|
// src/commands/init.ts
|
|
228
228
|
init_esm_shims();
|
|
@@ -7572,16 +7572,29 @@ var DETECTOR_FRAMEWORKS = {
|
|
|
7572
7572
|
"vault-token": ["APRA_CPS234", "DORA", "FISMA", "FedRAMP", "HITRUST", "ISO_27001", "ISO_27701", "NIS2", "NIST_800_171", "NIST_800_53", "NYDFS_500", "SOC2", "SOX"],
|
|
7573
7573
|
"vehicle-vin": ["APPI", "AU_PRIVACY", "CCPA_CPRA", "COPPA", "DPDP", "FADP", "FERPA", "GDPR", "HIPAA", "HITECH", "HITRUST", "ISO_27701", "LGPD", "PDPA_SG", "PDPA_TH", "PIPA_KR", "PIPEDA", "PIPL", "POPIA", "UK_GDPR"]
|
|
7574
7574
|
};
|
|
7575
|
+
var FRAMEWORK_ALIASES = {
|
|
7576
|
+
SOC_1: "SOX"
|
|
7577
|
+
};
|
|
7578
|
+
function canonicalFrameworkKey(input) {
|
|
7579
|
+
return input.trim().toUpperCase().replace(/[\s._-]+/g, "");
|
|
7580
|
+
}
|
|
7581
|
+
var ALIAS_BASE_BY_CANON = new Map(Object.entries(FRAMEWORK_ALIASES).map(([name, base]) => [canonicalFrameworkKey(name), base]));
|
|
7582
|
+
var POLICY_FRAMEWORKS = [
|
|
7583
|
+
...COMPLIANCE_FRAMEWORKS,
|
|
7584
|
+
...Object.keys(FRAMEWORK_ALIASES)
|
|
7585
|
+
];
|
|
7575
7586
|
function frameworksForDetector(detector) {
|
|
7576
7587
|
return Object.prototype.hasOwnProperty.call(DETECTOR_FRAMEWORKS, detector) ? DETECTOR_FRAMEWORKS[detector] : [];
|
|
7577
7588
|
}
|
|
7578
7589
|
function detectorsMatchFramework(detectors, framework) {
|
|
7579
|
-
const
|
|
7580
|
-
|
|
7590
|
+
const canon = canonicalFrameworkKey(framework);
|
|
7591
|
+
const base = ALIAS_BASE_BY_CANON.get(canon);
|
|
7592
|
+
const target = base ? canonicalFrameworkKey(base) : canon;
|
|
7593
|
+
return detectors.some((d) => frameworksForDetector(d).some((f) => canonicalFrameworkKey(f) === target));
|
|
7581
7594
|
}
|
|
7582
7595
|
function resolveFrameworkName(input) {
|
|
7583
|
-
const
|
|
7584
|
-
return
|
|
7596
|
+
const canon = canonicalFrameworkKey(input);
|
|
7597
|
+
return POLICY_FRAMEWORKS.find((f) => canonicalFrameworkKey(f) === canon) ?? null;
|
|
7585
7598
|
}
|
|
7586
7599
|
|
|
7587
7600
|
// src/compliance-preset.ts
|
|
@@ -7611,21 +7624,21 @@ function detectorsForFramework(framework) {
|
|
|
7611
7624
|
return ALL_TAGGED_DETECTORS.filter((d) => detectorsMatchFramework([d], framework));
|
|
7612
7625
|
}
|
|
7613
7626
|
function policyChoices() {
|
|
7614
|
-
return [...
|
|
7627
|
+
return [...POLICY_FRAMEWORKS, ...COMPLIANCE_PRESET_IDS];
|
|
7615
7628
|
}
|
|
7616
7629
|
function resolvePolicyName(input) {
|
|
7617
|
-
const
|
|
7618
|
-
const shortId = key.toLowerCase();
|
|
7630
|
+
const shortId = input.trim().toLowerCase();
|
|
7619
7631
|
const viaShortId = isCompliancePresetId(shortId) ? SHORT_PRESET_ALIASES[shortId] : void 0;
|
|
7620
|
-
const
|
|
7632
|
+
const canon = canonicalFrameworkKey(input);
|
|
7633
|
+
const framework = viaShortId ?? POLICY_FRAMEWORKS.find((f) => canonicalFrameworkKey(f) === canon);
|
|
7621
7634
|
if (!framework) {
|
|
7622
7635
|
throw new Error(
|
|
7623
|
-
`Unknown compliance preset: "${input}". Valid presets: ${COMPLIANCE_PRESET_IDS.join(", ")}. Any of the ${
|
|
7636
|
+
`Unknown compliance preset: "${input}". Valid presets: ${COMPLIANCE_PRESET_IDS.join(", ")}. Any of the ${POLICY_FRAMEWORKS.length} framework names from \`pretense audit --list-frameworks\` is also accepted (case- and separator-insensitive, e.g. pci-dss, PCI_DSS, pci_dss).`
|
|
7624
7637
|
);
|
|
7625
7638
|
}
|
|
7626
7639
|
const detectors = detectorsForFramework(framework);
|
|
7627
7640
|
const fingerprint = detectors.join("|");
|
|
7628
|
-
const sharedWith =
|
|
7641
|
+
const sharedWith = POLICY_FRAMEWORKS.filter(
|
|
7629
7642
|
(f) => f !== framework && detectorsForFramework(f).join("|") === fingerprint
|
|
7630
7643
|
);
|
|
7631
7644
|
const scannerPreset = SCANNER_PRESET_BY_FRAMEWORK[framework];
|
|
@@ -7818,7 +7831,7 @@ function startSpinner(text) {
|
|
|
7818
7831
|
|
|
7819
7832
|
// src/commands/start.ts
|
|
7820
7833
|
init_esm_shims();
|
|
7821
|
-
import { Command as Command2 } from "commander";
|
|
7834
|
+
import { Command as Command2, InvalidArgumentError } from "commander";
|
|
7822
7835
|
import chalk2 from "chalk";
|
|
7823
7836
|
import { execSync } from "child_process";
|
|
7824
7837
|
|
|
@@ -12002,6 +12015,70 @@ var EXTENDED_PATTERNS = [
|
|
|
12002
12015
|
validate: looksLikeSecretValue
|
|
12003
12016
|
}
|
|
12004
12017
|
];
|
|
12018
|
+
var THRESHOLD_HIGH_ENTROPY = 4.5;
|
|
12019
|
+
function shannonEntropy(str) {
|
|
12020
|
+
if (str.length === 0) return 0;
|
|
12021
|
+
const freq = {};
|
|
12022
|
+
for (const c of str) {
|
|
12023
|
+
freq[c] = (freq[c] ?? 0) + 1;
|
|
12024
|
+
}
|
|
12025
|
+
let entropy = 0;
|
|
12026
|
+
for (const count of Object.values(freq)) {
|
|
12027
|
+
const p = count / str.length;
|
|
12028
|
+
entropy -= p * Math.log2(p);
|
|
12029
|
+
}
|
|
12030
|
+
return entropy;
|
|
12031
|
+
}
|
|
12032
|
+
var MIN_ENTROPY_LENGTH = 20;
|
|
12033
|
+
var SECRET_CHARSET_RE = /^[A-Za-z0-9+/=_\-]+$/;
|
|
12034
|
+
var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
12035
|
+
var HEX_BLOB_RE = /^[a-f0-9]{32,}$/;
|
|
12036
|
+
var BASE64_BLOB_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
12037
|
+
var SECRET_KEYWORD_RE = /\b(?:password|passwd|pwd|secret|key|token|credential|creds?|api[_-]?key|apikey|auth|authorization|bearer|private[_-]?key|access[_-]?key|client[_-]?secret)\b/i;
|
|
12038
|
+
var SECRET_KEY_NAME_RE = /(?:password|passwd|pwd|secret|token|credential|cred|apikey|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key|authorization|bearer|\bauth\b|\bkey\b)/i;
|
|
12039
|
+
function isAllowlistedShape(value) {
|
|
12040
|
+
if (UUID_RE.test(value)) return true;
|
|
12041
|
+
if (HEX_BLOB_RE.test(value)) return true;
|
|
12042
|
+
if (BASE64_BLOB_RE.test(value) && value.length % 4 === 0 && !/[_-]/.test(value)) {
|
|
12043
|
+
return true;
|
|
12044
|
+
}
|
|
12045
|
+
return false;
|
|
12046
|
+
}
|
|
12047
|
+
function hasSecretContext(text, start, end) {
|
|
12048
|
+
const WINDOW = 80;
|
|
12049
|
+
const before = text.slice(Math.max(0, start - WINDOW), start);
|
|
12050
|
+
const after = text.slice(end, Math.min(text.length, end + WINDOW));
|
|
12051
|
+
if (SECRET_KEYWORD_RE.test(before) || SECRET_KEYWORD_RE.test(after)) {
|
|
12052
|
+
return true;
|
|
12053
|
+
}
|
|
12054
|
+
const assignMatch = before.match(/([A-Za-z0-9_$'"-]+)\s*[:=]\s*['"`]?\s*$/);
|
|
12055
|
+
if (assignMatch && SECRET_KEY_NAME_RE.test(assignMatch[1])) {
|
|
12056
|
+
return true;
|
|
12057
|
+
}
|
|
12058
|
+
return false;
|
|
12059
|
+
}
|
|
12060
|
+
function findHighEntropyStrings(text) {
|
|
12061
|
+
const matches = [];
|
|
12062
|
+
const tokenRe = /(?:['"`])([A-Za-z0-9+/=_\-]{20,})(?:['"`])/g;
|
|
12063
|
+
let m;
|
|
12064
|
+
while ((m = tokenRe.exec(text)) !== null) {
|
|
12065
|
+
const value = m[1];
|
|
12066
|
+
if (value.length < MIN_ENTROPY_LENGTH) continue;
|
|
12067
|
+
if (!SECRET_CHARSET_RE.test(value)) continue;
|
|
12068
|
+
const entropy = shannonEntropy(value);
|
|
12069
|
+
if (entropy < THRESHOLD_HIGH_ENTROPY) continue;
|
|
12070
|
+
const start = m.index + 1;
|
|
12071
|
+
const end = start + value.length;
|
|
12072
|
+
if (isAllowlistedShape(value) && !hasSecretContext(text, start, end)) {
|
|
12073
|
+
continue;
|
|
12074
|
+
}
|
|
12075
|
+
matches.push({ value, entropy, start, end });
|
|
12076
|
+
}
|
|
12077
|
+
return matches;
|
|
12078
|
+
}
|
|
12079
|
+
function awsSecretKeyIsHighEntropy(token) {
|
|
12080
|
+
return shannonEntropy(token) >= 3.9;
|
|
12081
|
+
}
|
|
12005
12082
|
function envAssignmentValueIsReal(match3) {
|
|
12006
12083
|
const sep2 = match3.search(/[=:]/);
|
|
12007
12084
|
if (sep2 === -1) return true;
|
|
@@ -12114,6 +12191,24 @@ var SECRET_PATTERNS = [
|
|
|
12114
12191
|
defaultAction: "block",
|
|
12115
12192
|
pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/g
|
|
12116
12193
|
},
|
|
12194
|
+
{
|
|
12195
|
+
// BLOCKER: the assignment-anchored `aws-secret-key` above misses the SECRET
|
|
12196
|
+
// access key when it appears in PROSE — "deploy with wJalrXUtnFEMI/K7MDENG/
|
|
12197
|
+
// bPxRfiCYEXAMPLEKEY" or next to its AKIA id — and the raw 40-char key then
|
|
12198
|
+
// reached the LLM verbatim (scan reported clean). This detector catches the
|
|
12199
|
+
// bare token, but ONLY within 80 chars of AWS context (an AKIA id, "aws
|
|
12200
|
+
// secret", or "secret access key"), so an unrelated 40-char base64 string is
|
|
12201
|
+
// not swept up. `valueGroup: 1` scopes the block/tokenization to the token,
|
|
12202
|
+
// never the surrounding words; the `d` flag is REQUIRED for that span, and
|
|
12203
|
+
// the entropy `validate` is the secondary FP guard (rejects a git SHA).
|
|
12204
|
+
name: "aws-secret-access-key-contextual",
|
|
12205
|
+
category: "secret",
|
|
12206
|
+
severity: "critical",
|
|
12207
|
+
defaultAction: "block",
|
|
12208
|
+
pattern: /(?:AKIA[0-9A-Z]{16}|aws[_\s-]{0,3}secret(?:[_\s-]{0,3}access)?[_\s-]{0,3}key|secret[_\s-]{0,3}access[_\s-]{0,3}key)[\s\S]{0,80}?(?<![A-Za-z0-9/+])([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+=])/gid,
|
|
12209
|
+
valueGroup: 1,
|
|
12210
|
+
validate: awsSecretKeyIsHighEntropy
|
|
12211
|
+
},
|
|
12117
12212
|
{
|
|
12118
12213
|
name: "gcp-service-account",
|
|
12119
12214
|
category: "secret",
|
|
@@ -12787,67 +12882,6 @@ var ALL_PATTERNS = [...SECRET_PATTERNS, ...PII_PATTERNS, ...ENV_PATTERNS, ...EXT
|
|
|
12787
12882
|
function getPatternCount() {
|
|
12788
12883
|
return ALL_PATTERNS.length;
|
|
12789
12884
|
}
|
|
12790
|
-
var THRESHOLD_HIGH_ENTROPY = 4.5;
|
|
12791
|
-
function shannonEntropy(str) {
|
|
12792
|
-
if (str.length === 0) return 0;
|
|
12793
|
-
const freq = {};
|
|
12794
|
-
for (const c of str) {
|
|
12795
|
-
freq[c] = (freq[c] ?? 0) + 1;
|
|
12796
|
-
}
|
|
12797
|
-
let entropy = 0;
|
|
12798
|
-
for (const count of Object.values(freq)) {
|
|
12799
|
-
const p = count / str.length;
|
|
12800
|
-
entropy -= p * Math.log2(p);
|
|
12801
|
-
}
|
|
12802
|
-
return entropy;
|
|
12803
|
-
}
|
|
12804
|
-
var MIN_ENTROPY_LENGTH = 20;
|
|
12805
|
-
var SECRET_CHARSET_RE = /^[A-Za-z0-9+/=_\-]+$/;
|
|
12806
|
-
var UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
12807
|
-
var HEX_BLOB_RE = /^[a-f0-9]{32,}$/;
|
|
12808
|
-
var BASE64_BLOB_RE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
12809
|
-
var SECRET_KEYWORD_RE = /\b(?:password|passwd|pwd|secret|key|token|credential|creds?|api[_-]?key|apikey|auth|authorization|bearer|private[_-]?key|access[_-]?key|client[_-]?secret)\b/i;
|
|
12810
|
-
var SECRET_KEY_NAME_RE = /(?:password|passwd|pwd|secret|token|credential|cred|apikey|api[_-]?key|access[_-]?key|client[_-]?secret|private[_-]?key|authorization|bearer|\bauth\b|\bkey\b)/i;
|
|
12811
|
-
function isAllowlistedShape(value) {
|
|
12812
|
-
if (UUID_RE.test(value)) return true;
|
|
12813
|
-
if (HEX_BLOB_RE.test(value)) return true;
|
|
12814
|
-
if (BASE64_BLOB_RE.test(value) && value.length % 4 === 0 && !/[_-]/.test(value)) {
|
|
12815
|
-
return true;
|
|
12816
|
-
}
|
|
12817
|
-
return false;
|
|
12818
|
-
}
|
|
12819
|
-
function hasSecretContext(text, start, end) {
|
|
12820
|
-
const WINDOW = 80;
|
|
12821
|
-
const before = text.slice(Math.max(0, start - WINDOW), start);
|
|
12822
|
-
const after = text.slice(end, Math.min(text.length, end + WINDOW));
|
|
12823
|
-
if (SECRET_KEYWORD_RE.test(before) || SECRET_KEYWORD_RE.test(after)) {
|
|
12824
|
-
return true;
|
|
12825
|
-
}
|
|
12826
|
-
const assignMatch = before.match(/([A-Za-z0-9_$'"-]+)\s*[:=]\s*['"`]?\s*$/);
|
|
12827
|
-
if (assignMatch && SECRET_KEY_NAME_RE.test(assignMatch[1])) {
|
|
12828
|
-
return true;
|
|
12829
|
-
}
|
|
12830
|
-
return false;
|
|
12831
|
-
}
|
|
12832
|
-
function findHighEntropyStrings(text) {
|
|
12833
|
-
const matches = [];
|
|
12834
|
-
const tokenRe = /(?:['"`])([A-Za-z0-9+/=_\-]{20,})(?:['"`])/g;
|
|
12835
|
-
let m;
|
|
12836
|
-
while ((m = tokenRe.exec(text)) !== null) {
|
|
12837
|
-
const value = m[1];
|
|
12838
|
-
if (value.length < MIN_ENTROPY_LENGTH) continue;
|
|
12839
|
-
if (!SECRET_CHARSET_RE.test(value)) continue;
|
|
12840
|
-
const entropy = shannonEntropy(value);
|
|
12841
|
-
if (entropy < THRESHOLD_HIGH_ENTROPY) continue;
|
|
12842
|
-
const start = m.index + 1;
|
|
12843
|
-
const end = start + value.length;
|
|
12844
|
-
if (isAllowlistedShape(value) && !hasSecretContext(text, start, end)) {
|
|
12845
|
-
continue;
|
|
12846
|
-
}
|
|
12847
|
-
matches.push({ value, entropy, start, end });
|
|
12848
|
-
}
|
|
12849
|
-
return matches;
|
|
12850
|
-
}
|
|
12851
12885
|
function icd10Check(match3) {
|
|
12852
12886
|
const code = match3.trim().toUpperCase();
|
|
12853
12887
|
return /^[A-Z]\d{2}(?:\.[A-Z0-9]{1,4})?$/.test(code);
|
|
@@ -16118,6 +16152,7 @@ var AuditStore = class {
|
|
|
16118
16152
|
blocked: 0,
|
|
16119
16153
|
mutated: 0,
|
|
16120
16154
|
redacted: 0,
|
|
16155
|
+
reversed: 0,
|
|
16121
16156
|
error: 0
|
|
16122
16157
|
};
|
|
16123
16158
|
for (const row of actionRows) {
|
|
@@ -16445,11 +16480,11 @@ function versionFromCliPackageJson() {
|
|
|
16445
16480
|
return void 0;
|
|
16446
16481
|
}
|
|
16447
16482
|
function productVersion() {
|
|
16448
|
-
return override ?? (true ? "0.6.
|
|
16483
|
+
return override ?? (true ? "0.6.16" : void 0) ?? versionFromCliPackageJson() ?? "0.0.0-unknown";
|
|
16449
16484
|
}
|
|
16450
16485
|
var UNKNOWN_COMMIT = "unknown";
|
|
16451
16486
|
function bakedCommitSha() {
|
|
16452
|
-
return "
|
|
16487
|
+
return "65c9faca2983b357369c6f91aa0cad0cf3a70b82".length > 0 ? "65c9faca2983b357369c6f91aa0cad0cf3a70b82" : UNKNOWN_COMMIT;
|
|
16453
16488
|
}
|
|
16454
16489
|
var FEATURE_TIERS = {
|
|
16455
16490
|
compliance_export: "pro",
|
|
@@ -16571,7 +16606,10 @@ var PlanEnforcer = class {
|
|
|
16571
16606
|
upgradeMessage,
|
|
16572
16607
|
upgradeTier,
|
|
16573
16608
|
upgradeUrl,
|
|
16574
|
-
resetDate
|
|
16609
|
+
resetDate,
|
|
16610
|
+
limitMetric: worstMetric,
|
|
16611
|
+
limitCurrent: worstCurrent,
|
|
16612
|
+
limitValue: worstLimit
|
|
16575
16613
|
};
|
|
16576
16614
|
}
|
|
16577
16615
|
if (overallPercent >= 90) {
|
|
@@ -17514,20 +17552,26 @@ function createProxy(opts = {}) {
|
|
|
17514
17552
|
if (_enforcement.upgradeUrl) c.header("X-Pretense-Upgrade-Url", _enforcement.upgradeUrl);
|
|
17515
17553
|
}
|
|
17516
17554
|
if (!_enforcement.allowed) {
|
|
17517
|
-
const _planConfig = PLAN_CONFIGS[_effectiveTier];
|
|
17518
|
-
const _mutLimit = _planConfig.mutationsPerPeriod ?? 0;
|
|
17519
|
-
const _mutCurrent = _periodStats.totalMutationsApplied;
|
|
17520
17555
|
const _upgradeTier = _enforcement.upgradeTier ?? "pro";
|
|
17556
|
+
const _metric = _enforcement.limitMetric ?? "mutations";
|
|
17557
|
+
const _mutCurrent = _enforcement.limitCurrent ?? _periodStats.totalMutationsApplied;
|
|
17558
|
+
const _mutLimit = _enforcement.limitValue ?? (PLAN_CONFIGS[_effectiveTier].mutationsPerPeriod ?? 0);
|
|
17559
|
+
const _metricLabel = {
|
|
17560
|
+
mutations: "Mutation",
|
|
17561
|
+
scans: "Request",
|
|
17562
|
+
bytes: "Data",
|
|
17563
|
+
seats: "Seat"
|
|
17564
|
+
};
|
|
17565
|
+
const _label = _metricLabel[_metric] ?? "Usage";
|
|
17521
17566
|
return c.json({
|
|
17522
17567
|
error: {
|
|
17523
17568
|
type: "plan_limit_exceeded",
|
|
17524
|
-
code:
|
|
17525
|
-
// Human-readable error matching the spec format
|
|
17569
|
+
code: `${_metric.toUpperCase()}_LIMIT_EXCEEDED`,
|
|
17526
17570
|
// 100,000 = PLAN_CONFIGS.pro.mutationsPerPeriod; $29/seat =
|
|
17527
17571
|
// PLAN_CONFIGS.pro.pricePerSeatCents (monthly rate). Enterprise
|
|
17528
17572
|
// prints no price and no self-serve `pretense upgrade` path: it
|
|
17529
17573
|
// is contact-sales only. DO NOT restore "$99/seat".
|
|
17530
|
-
message: `\u2717
|
|
17574
|
+
message: `\u2717 ${_label} limit reached for this 7-day period (${_mutCurrent.toLocaleString()}/${_mutLimit.toLocaleString()})
|
|
17531
17575
|
|
|
17532
17576
|
${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014 contact sales for a quote.\n\u2192 visit pretense.ai/pricing" : "Upgrade to Pro for 100,000 mutations / 7 days ($29/seat)\n\u2192 pretense upgrade OR visit pretense.ai/pricing"}`,
|
|
17533
17577
|
upgrade: {
|
|
@@ -17540,7 +17584,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17540
17584
|
current: _mutCurrent,
|
|
17541
17585
|
limit: _mutLimit,
|
|
17542
17586
|
percent: _enforcement.usagePercent,
|
|
17543
|
-
metric:
|
|
17587
|
+
metric: _metric,
|
|
17544
17588
|
resetDate: _enforcement.resetDate
|
|
17545
17589
|
}
|
|
17546
17590
|
}
|
|
@@ -17681,6 +17725,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17681
17725
|
return replaceCodeBlocks(text, blocks, mutatedBlocks);
|
|
17682
17726
|
});
|
|
17683
17727
|
const mutDurationMs = Math.round((performance.now() - mutT0) * 100) / 100;
|
|
17728
|
+
const mutationsApplied = tokensMutated + secretsTokenized + piiTokenized;
|
|
17684
17729
|
const totalChars = fullText.length;
|
|
17685
17730
|
const identifierExposureRate = totalChars > 0 ? Math.min(1, tokensMutated / Math.max(1, totalChars / 10)) : 0;
|
|
17686
17731
|
const riskCtx = {
|
|
@@ -17709,7 +17754,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17709
17754
|
processedSize: 0,
|
|
17710
17755
|
secretsBlocked: secretsTokenized,
|
|
17711
17756
|
piiRedacted: piiTokenized,
|
|
17712
|
-
mutationsApplied
|
|
17757
|
+
mutationsApplied,
|
|
17713
17758
|
scanDurationMs,
|
|
17714
17759
|
mutationDurationMs: mutDurationMs,
|
|
17715
17760
|
action: "blocked",
|
|
@@ -17791,7 +17836,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17791
17836
|
processedSize,
|
|
17792
17837
|
secretsBlocked: secretsTokenized,
|
|
17793
17838
|
piiRedacted: piiTokenized,
|
|
17794
|
-
mutationsApplied
|
|
17839
|
+
mutationsApplied,
|
|
17795
17840
|
scanDurationMs,
|
|
17796
17841
|
mutationDurationMs: mutDurationMs,
|
|
17797
17842
|
action: "error",
|
|
@@ -17828,7 +17873,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17828
17873
|
processedSize,
|
|
17829
17874
|
secretsBlocked: secretsTokenized,
|
|
17830
17875
|
piiRedacted: piiTokenized,
|
|
17831
|
-
mutationsApplied
|
|
17876
|
+
mutationsApplied,
|
|
17832
17877
|
scanDurationMs,
|
|
17833
17878
|
mutationDurationMs: mutDurationMs,
|
|
17834
17879
|
action,
|
|
@@ -17904,10 +17949,16 @@ function renderClientEnvLines(port, protocol) {
|
|
|
17904
17949
|
" Easiest \u2014 let Pretense wire the env var and run the tool for you:",
|
|
17905
17950
|
' pretense run claude "build me X" # or: cursor, gemini, any tool',
|
|
17906
17951
|
"",
|
|
17907
|
-
|
|
17908
|
-
`
|
|
17909
|
-
|
|
17910
|
-
`
|
|
17952
|
+
// The manual lines are SAME-SHELL (env var and tool on one command) on
|
|
17953
|
+
// purpose: a user who runs `export …BASE_URL=…` here and then `claude` in
|
|
17954
|
+
// a NEW terminal is unprotected — env vars are per-shell. The warning
|
|
17955
|
+
// below states that, and points back at `pretense run` as the fix.
|
|
17956
|
+
" Or point your AI client at Pretense yourself (SAME terminal as the tool):",
|
|
17957
|
+
` ANTHROPIC_BASE_URL=${base} claude`,
|
|
17958
|
+
` OPENAI_BASE_URL=${base}/v1 cursor`,
|
|
17959
|
+
` GOOGLE_GEMINI_BASE_URL=${base} gemini`,
|
|
17960
|
+
" \u26A0 Set this in the SAME terminal as your AI tool \u2014 a new terminal is NOT",
|
|
17961
|
+
" protected. Simplest: use `pretense run` and skip the export entirely."
|
|
17911
17962
|
].join("\n") + "\n";
|
|
17912
17963
|
}
|
|
17913
17964
|
function printBanner(port, protocol, tls) {
|
|
@@ -18051,6 +18102,16 @@ async function stopProxy(path2 = pidFilePath(), graceMs = 2e3, pollMs = 50) {
|
|
|
18051
18102
|
}
|
|
18052
18103
|
|
|
18053
18104
|
// src/commands/start.ts
|
|
18105
|
+
function parsePort(raw2) {
|
|
18106
|
+
if (!/^\d+$/.test(raw2.trim())) {
|
|
18107
|
+
throw new InvalidArgumentError(`"${raw2}" is not a number. Use a port between 1 and 65535.`);
|
|
18108
|
+
}
|
|
18109
|
+
const n = Number(raw2);
|
|
18110
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
18111
|
+
throw new InvalidArgumentError(`${raw2} is out of range. Use a port between 1 and 65535.`);
|
|
18112
|
+
}
|
|
18113
|
+
return String(n);
|
|
18114
|
+
}
|
|
18054
18115
|
function pidListeningOn(port) {
|
|
18055
18116
|
try {
|
|
18056
18117
|
const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, {
|
|
@@ -18084,7 +18145,7 @@ function explainAddrInUse(port) {
|
|
|
18084
18145
|
);
|
|
18085
18146
|
}
|
|
18086
18147
|
function startCommand() {
|
|
18087
|
-
return new Command2("start").description("Start the Pretense proxy server").option("-p, --port <port>", "Port to listen on", "9339").option("--verbose", "Enable verbose logging", false).option("-c, --config <path>", "Path to pretense.yaml").option("--strict-port", "Fail if the requested port is busy instead of auto-advancing", false).action(async (opts) => {
|
|
18148
|
+
return new Command2("start").description("Start the Pretense proxy server").option("-p, --port <port>", "Port to listen on (1\u201365535)", parsePort, "9339").option("--verbose", "Enable verbose logging", false).option("-c, --config <path>", "Path to pretense.yaml").option("--strict-port", "Fail if the requested port is busy instead of auto-advancing", false).action(async (opts) => {
|
|
18088
18149
|
const requested = parseInt(opts.port, 10);
|
|
18089
18150
|
const existing = readProxyRecord();
|
|
18090
18151
|
if (existing) {
|
|
@@ -18092,7 +18153,10 @@ function startCommand() {
|
|
|
18092
18153
|
if (existing.port === requested) {
|
|
18093
18154
|
console.error(
|
|
18094
18155
|
chalk2.red(`
|
|
18095
|
-
x Pretense proxy is already running (pid ${existing.pid}, port ${existing.port}).`) +
|
|
18156
|
+
x Pretense proxy is already running (pid ${existing.pid}, port ${existing.port}).`) + // The proxy is UP — the next thing the user wants is to route a
|
|
18157
|
+
// tool through it, and `pretense run` reuses this exact proxy
|
|
18158
|
+
// (no second export, no second terminal). Lead with that.
|
|
18159
|
+
chalk2.dim("\n Route a tool through it with ") + chalk2.cyan('pretense run claude "\u2026"') + chalk2.dim(" (or cursor, gemini, any tool).") + chalk2.dim("\n Stop it with ") + chalk2.cyan("pretense stop") + chalk2.dim(", or start this one on another port with ") + chalk2.cyan(`--port ${requested + 1}`) + "\n"
|
|
18096
18160
|
);
|
|
18097
18161
|
process.exit(1);
|
|
18098
18162
|
}
|
|
@@ -18155,114 +18219,490 @@ function startCommand() {
|
|
|
18155
18219
|
|
|
18156
18220
|
// src/commands/stop.ts
|
|
18157
18221
|
init_esm_shims();
|
|
18158
|
-
import { Command as
|
|
18159
|
-
import chalk3 from "chalk";
|
|
18160
|
-
function stopCommand() {
|
|
18161
|
-
return new Command3("stop").description("Stop the running Pretense proxy").option("--json", "Machine-readable outcome", false).action(async (opts) => {
|
|
18162
|
-
const path2 = pidFilePath();
|
|
18163
|
-
const outcome = await stopProxy(path2);
|
|
18164
|
-
if (opts.json) {
|
|
18165
|
-
console.log(JSON.stringify(outcome, null, 2));
|
|
18166
|
-
process.exit(outcome.kind === "refused" || outcome.kind === "not-permitted" ? 1 : 0);
|
|
18167
|
-
}
|
|
18168
|
-
switch (outcome.kind) {
|
|
18169
|
-
case "not-running":
|
|
18170
|
-
console.log(
|
|
18171
|
-
chalk3.dim(`
|
|
18172
|
-
\xB7 No running Pretense proxy recorded (${path2} does not exist).`) + chalk3.dim(`
|
|
18173
|
-
Start one with `) + chalk3.cyan("pretense start") + "\n"
|
|
18174
|
-
);
|
|
18175
|
-
process.exit(0);
|
|
18176
|
-
break;
|
|
18177
|
-
case "stale":
|
|
18178
|
-
console.log(
|
|
18179
|
-
chalk3.yellow(
|
|
18180
|
-
`
|
|
18181
|
-
\xB7 Stale PID file cleaned up${outcome.pid ? ` (pid ${outcome.pid} is not running)` : ""}.`
|
|
18182
|
-
) + chalk3.dim(`
|
|
18183
|
-
No proxy was running.
|
|
18184
|
-
`)
|
|
18185
|
-
);
|
|
18186
|
-
process.exit(0);
|
|
18187
|
-
break;
|
|
18188
|
-
case "stopped":
|
|
18189
|
-
console.log(
|
|
18190
|
-
chalk3.green(
|
|
18191
|
-
`
|
|
18192
|
-
\u2713 Pretense proxy stopped (pid ${outcome.pid}${outcome.port ? `, port ${outcome.port}` : ""})` + (outcome.forced ? " \u2014 did not exit on SIGTERM, sent SIGKILL" : "") + "\n"
|
|
18193
|
-
)
|
|
18194
|
-
);
|
|
18195
|
-
process.exit(0);
|
|
18196
|
-
break;
|
|
18197
|
-
case "not-permitted":
|
|
18198
|
-
console.error(
|
|
18199
|
-
chalk3.red(`
|
|
18200
|
-
x Cannot stop pid ${outcome.pid}: it belongs to another user.`) + chalk3.dim(`
|
|
18201
|
-
The proxy is still running. Stop it as its owner, or: sudo kill ${outcome.pid}
|
|
18202
|
-
`)
|
|
18203
|
-
);
|
|
18204
|
-
process.exit(1);
|
|
18205
|
-
break;
|
|
18206
|
-
case "refused":
|
|
18207
|
-
console.error(
|
|
18208
|
-
chalk3.red(`
|
|
18209
|
-
x Pid ${outcome.pid} is still alive after SIGTERM and SIGKILL.`) + chalk3.dim(`
|
|
18210
|
-
${path2} was left in place so you can investigate: ps -p ${outcome.pid}
|
|
18211
|
-
`)
|
|
18212
|
-
);
|
|
18213
|
-
process.exit(1);
|
|
18214
|
-
break;
|
|
18215
|
-
}
|
|
18216
|
-
});
|
|
18217
|
-
}
|
|
18218
|
-
|
|
18219
|
-
// src/commands/scan.ts
|
|
18220
|
-
init_esm_shims();
|
|
18221
|
-
import { Command as Command4, Option, InvalidArgumentError } from "commander";
|
|
18222
|
+
import { Command as Command4 } from "commander";
|
|
18222
18223
|
import chalk4 from "chalk";
|
|
18223
|
-
import { readFileSync as readFileSync7, statSync as statSync3, readdirSync as readdirSync2, openSync, readSync, closeSync } from "fs";
|
|
18224
18224
|
import { execSync as execSync2 } from "child_process";
|
|
18225
|
-
import { resolve as resolve2, join as join7, extname as extname2, basename } from "path";
|
|
18226
18225
|
|
|
18227
|
-
// src/
|
|
18226
|
+
// src/commands/doctor.ts
|
|
18228
18227
|
init_esm_shims();
|
|
18229
|
-
|
|
18230
|
-
|
|
18231
|
-
|
|
18232
|
-
|
|
18233
|
-
|
|
18234
|
-
|
|
18235
|
-
|
|
18236
|
-
|
|
18237
|
-
|
|
18238
|
-
|
|
18239
|
-
}
|
|
18240
|
-
return { line, column: clamped - lineStart + 1 };
|
|
18241
|
-
}
|
|
18242
|
-
function withLocations(result, content) {
|
|
18243
|
-
const locate = (m) => lineColFromOffset2(content, m.start);
|
|
18244
|
-
return {
|
|
18245
|
-
...result,
|
|
18246
|
-
matches: result.matches.map((m) => ({ ...m, ...locate(m) }))
|
|
18247
|
-
};
|
|
18228
|
+
import { Command as Command3 } from "commander";
|
|
18229
|
+
import chalk3 from "chalk";
|
|
18230
|
+
|
|
18231
|
+
// src/plan-usage.ts
|
|
18232
|
+
init_esm_shims();
|
|
18233
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
18234
|
+
import { join as join7 } from "path";
|
|
18235
|
+
import { homedir as homedir4 } from "os";
|
|
18236
|
+
function credentialsPath() {
|
|
18237
|
+
return join7(homedir4(), ".pretense", "credentials.json");
|
|
18248
18238
|
}
|
|
18249
|
-
function
|
|
18250
|
-
const
|
|
18251
|
-
if (
|
|
18252
|
-
const
|
|
18253
|
-
|
|
18239
|
+
function readApiKey() {
|
|
18240
|
+
const fromEnv = process.env["PRETENSE_API_KEY"]?.trim();
|
|
18241
|
+
if (fromEnv) return fromEnv;
|
|
18242
|
+
const path2 = credentialsPath();
|
|
18243
|
+
if (!existsSync8(path2)) return null;
|
|
18244
|
+
try {
|
|
18245
|
+
const creds = JSON.parse(readFileSync7(path2, "utf-8"));
|
|
18246
|
+
const key = creds["apiKey"] ?? creds["api_key"];
|
|
18247
|
+
if (typeof key === "string" && key.trim().length > 0) return key.trim();
|
|
18248
|
+
} catch {
|
|
18249
|
+
}
|
|
18250
|
+
return null;
|
|
18254
18251
|
}
|
|
18255
|
-
var
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18252
|
+
var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
|
|
18253
|
+
var DEFAULT_TIER_TIMEOUT_MS = 5e3;
|
|
18254
|
+
function getConfiguredApiRoot() {
|
|
18255
|
+
const raw2 = (process.env["PRETENSE_API_URL"] ?? DEFAULT_PRETENSE_API_URL).trim();
|
|
18256
|
+
if (!raw2) return DEFAULT_PRETENSE_API_URL;
|
|
18257
|
+
const normalized = /^https?:\/\//i.test(raw2) ? raw2 : `https://${raw2}`;
|
|
18258
|
+
try {
|
|
18259
|
+
const u = new URL(normalized);
|
|
18260
|
+
const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL : u.origin;
|
|
18261
|
+
const path2 = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
18262
|
+
return `${origin}${path2}`;
|
|
18263
|
+
} catch {
|
|
18264
|
+
return DEFAULT_PRETENSE_API_URL;
|
|
18261
18265
|
}
|
|
18262
|
-
out.length = typeof row.end === "number" && typeof row.start === "number" ? row.end - row.start : 0;
|
|
18263
|
-
return out;
|
|
18264
18266
|
}
|
|
18265
|
-
function
|
|
18267
|
+
function tierRequestTimeoutMs() {
|
|
18268
|
+
const raw2 = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
|
|
18269
|
+
if (!raw2 || !/^\d+$/.test(raw2)) return DEFAULT_TIER_TIMEOUT_MS;
|
|
18270
|
+
return Math.min(Math.max(parseInt(raw2, 10), 1e3), 12e4);
|
|
18271
|
+
}
|
|
18272
|
+
function planToTier(plan) {
|
|
18273
|
+
switch (String(plan).toUpperCase()) {
|
|
18274
|
+
case "FREE":
|
|
18275
|
+
return "free";
|
|
18276
|
+
case "PRO":
|
|
18277
|
+
return "pro";
|
|
18278
|
+
case "ENTERPRISE":
|
|
18279
|
+
return "enterprise";
|
|
18280
|
+
default:
|
|
18281
|
+
return null;
|
|
18282
|
+
}
|
|
18283
|
+
}
|
|
18284
|
+
async function tierFromBackend(apiKey) {
|
|
18285
|
+
const url = `${getConfiguredApiRoot()}/api/cli/auth/validate`;
|
|
18286
|
+
const controller = new AbortController();
|
|
18287
|
+
const timer = setTimeout(() => controller.abort(), tierRequestTimeoutMs());
|
|
18288
|
+
try {
|
|
18289
|
+
const resp = await fetch(url, {
|
|
18290
|
+
method: "POST",
|
|
18291
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
18292
|
+
body: "{}",
|
|
18293
|
+
signal: controller.signal
|
|
18294
|
+
});
|
|
18295
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
18296
|
+
return {
|
|
18297
|
+
tier: null,
|
|
18298
|
+
source: "unverified",
|
|
18299
|
+
detail: "the server rejected this API key \u2014 run `pretense auth login` again"
|
|
18300
|
+
};
|
|
18301
|
+
}
|
|
18302
|
+
if (!resp.ok) {
|
|
18303
|
+
return {
|
|
18304
|
+
tier: null,
|
|
18305
|
+
source: "unverified",
|
|
18306
|
+
detail: `the plan service returned HTTP ${resp.status}`
|
|
18307
|
+
};
|
|
18308
|
+
}
|
|
18309
|
+
const body = await resp.json();
|
|
18310
|
+
const tier = planToTier(body["plan"]);
|
|
18311
|
+
if (body["valid"] === false || !tier) {
|
|
18312
|
+
return {
|
|
18313
|
+
tier: null,
|
|
18314
|
+
source: "unverified",
|
|
18315
|
+
detail: "the plan service did not report a recognizable plan"
|
|
18316
|
+
};
|
|
18317
|
+
}
|
|
18318
|
+
return { tier, source: "backend" };
|
|
18319
|
+
} catch (err) {
|
|
18320
|
+
const aborted = err?.name === "AbortError";
|
|
18321
|
+
return {
|
|
18322
|
+
tier: null,
|
|
18323
|
+
source: "unverified",
|
|
18324
|
+
detail: aborted ? `could not reach ${url} within ${tierRequestTimeoutMs()}ms` : `could not reach ${url}`
|
|
18325
|
+
};
|
|
18326
|
+
} finally {
|
|
18327
|
+
clearTimeout(timer);
|
|
18328
|
+
}
|
|
18329
|
+
}
|
|
18330
|
+
async function resolvePlanTier(opts = {}) {
|
|
18331
|
+
const apiKey = readApiKey();
|
|
18332
|
+
if (!apiKey) return { tier: "free", source: "no-credentials" };
|
|
18333
|
+
const fromPrefix = tierFromApiKey(apiKey);
|
|
18334
|
+
if (fromPrefix) return { tier: fromPrefix, source: "key-prefix" };
|
|
18335
|
+
if (opts.offline) {
|
|
18336
|
+
return {
|
|
18337
|
+
tier: null,
|
|
18338
|
+
source: "unverified",
|
|
18339
|
+
detail: "--offline was set, and a dashboard key carries no plan of its own"
|
|
18340
|
+
};
|
|
18341
|
+
}
|
|
18342
|
+
return tierFromBackend(apiKey);
|
|
18343
|
+
}
|
|
18344
|
+
var QUOTA_PERIOD_DAYS2 = 7;
|
|
18345
|
+
function collectUsage(dbPath) {
|
|
18346
|
+
const now = /* @__PURE__ */ new Date();
|
|
18347
|
+
const periodMs = QUOTA_PERIOD_DAYS2 * 24 * 60 * 60 * 1e3;
|
|
18348
|
+
const periodStartTs = Date.now() - periodMs;
|
|
18349
|
+
const sinceMs = periodMs;
|
|
18350
|
+
let store = null;
|
|
18351
|
+
try {
|
|
18352
|
+
store = new AuditStore(dbPath);
|
|
18353
|
+
const stats = store.getStats(sinceMs);
|
|
18354
|
+
return {
|
|
18355
|
+
periodStartTs,
|
|
18356
|
+
metrics: {
|
|
18357
|
+
tenantId: "local",
|
|
18358
|
+
// The trailing window these numbers actually cover — previously the
|
|
18359
|
+
// calendar month, which did not match the aggregation OR the quota.
|
|
18360
|
+
periodStart: new Date(periodStartTs).toISOString().split("T")[0],
|
|
18361
|
+
periodEnd: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
18362
|
+
mutationsApplied: stats.totalMutationsApplied,
|
|
18363
|
+
secretsBlocked: stats.totalSecretsBlocked,
|
|
18364
|
+
piiRedacted: stats.totalPiiRedacted,
|
|
18365
|
+
totalScans: stats.totalRequests,
|
|
18366
|
+
bytesProtected: 0,
|
|
18367
|
+
activeSeats: 1,
|
|
18368
|
+
providers: Object.fromEntries(stats.topProviders.map((p) => [p.provider, p.count]))
|
|
18369
|
+
}
|
|
18370
|
+
};
|
|
18371
|
+
} catch {
|
|
18372
|
+
return null;
|
|
18373
|
+
} finally {
|
|
18374
|
+
store?.close();
|
|
18375
|
+
}
|
|
18376
|
+
}
|
|
18377
|
+
function resetCadenceLabel() {
|
|
18378
|
+
return `every ${QUOTA_PERIOD_DAYS2} days`;
|
|
18379
|
+
}
|
|
18380
|
+
function priceLabel(tier) {
|
|
18381
|
+
const cents = PLAN_CONFIGS[tier].pricePerSeatCents;
|
|
18382
|
+
if (cents === null) return "custom \u2014 contact sales";
|
|
18383
|
+
return cents === 0 ? "free" : `$${cents / 100}/seat/month`;
|
|
18384
|
+
}
|
|
18385
|
+
function tierName(tier) {
|
|
18386
|
+
return { free: "Free", pro: "Pro", enterprise: "Enterprise" }[tier];
|
|
18387
|
+
}
|
|
18388
|
+
function tierLabel(tier) {
|
|
18389
|
+
if (tier === null) return "Unknown (unverified)";
|
|
18390
|
+
if (tier === "free") return "Free";
|
|
18391
|
+
return `${tierName(tier)} (${priceLabel(tier)})`;
|
|
18392
|
+
}
|
|
18393
|
+
function planMismatchHint(resolution) {
|
|
18394
|
+
if (resolution.tier !== "free" || resolution.source !== "backend") return [];
|
|
18395
|
+
return [
|
|
18396
|
+
"Not the plan you expected? An org flagged PRO without an active",
|
|
18397
|
+
"subscription/comp resolves to Free \u2014 see pretense.ai/dashboard/settings or contact support."
|
|
18398
|
+
];
|
|
18399
|
+
}
|
|
18400
|
+
function unverifiedNote(detail) {
|
|
18401
|
+
return [
|
|
18402
|
+
`Could not confirm your plan: ${detail ?? "unknown reason"}.`,
|
|
18403
|
+
"Limits are not shown because guessing could under- or over-state what you pay for."
|
|
18404
|
+
];
|
|
18405
|
+
}
|
|
18406
|
+
|
|
18407
|
+
// src/commands/doctor.ts
|
|
18408
|
+
var BASE_URL_ENVS = [
|
|
18409
|
+
{ name: "ANTHROPIC_BASE_URL", provider: "Claude / Claude Code" },
|
|
18410
|
+
{ name: "OPENAI_BASE_URL", provider: "OpenAI / Cursor / any OpenAI-compatible" },
|
|
18411
|
+
{ name: "GEMINI_BASE_URL", provider: "Google Gemini" },
|
|
18412
|
+
{ name: "GOOGLE_GEMINI_BASE_URL", provider: "Google Gemini (legacy var)" }
|
|
18413
|
+
];
|
|
18414
|
+
var COMMON_PORTS = Array.from({ length: 7 }, (_, i) => 9339 + i);
|
|
18415
|
+
async function probePort(port, timeoutMs = 700) {
|
|
18416
|
+
const ctrl = new AbortController();
|
|
18417
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
18418
|
+
try {
|
|
18419
|
+
const res = await fetch(`http://localhost:${port}/health`, { signal: ctrl.signal });
|
|
18420
|
+
if (!res.ok) return { alive: true, isPretense: false };
|
|
18421
|
+
const hasHeader = res.headers.has("x-pretense-version");
|
|
18422
|
+
let body = {};
|
|
18423
|
+
try {
|
|
18424
|
+
body = await res.json();
|
|
18425
|
+
} catch {
|
|
18426
|
+
return { alive: true, isPretense: false };
|
|
18427
|
+
}
|
|
18428
|
+
const looksPretense = hasHeader && "auditAvailable" in body && "version" in body;
|
|
18429
|
+
return {
|
|
18430
|
+
alive: true,
|
|
18431
|
+
isPretense: looksPretense,
|
|
18432
|
+
status: typeof body["status"] === "string" ? body["status"] : void 0,
|
|
18433
|
+
auditAvailable: typeof body["auditAvailable"] === "boolean" ? body["auditAvailable"] : void 0
|
|
18434
|
+
};
|
|
18435
|
+
} catch {
|
|
18436
|
+
return { alive: false, isPretense: false };
|
|
18437
|
+
} finally {
|
|
18438
|
+
clearTimeout(t);
|
|
18439
|
+
}
|
|
18440
|
+
}
|
|
18441
|
+
function portFromBaseUrl(value) {
|
|
18442
|
+
try {
|
|
18443
|
+
const u = new URL(value);
|
|
18444
|
+
if (u.port) return parseInt(u.port, 10);
|
|
18445
|
+
return u.protocol === "https:" ? 443 : 80;
|
|
18446
|
+
} catch {
|
|
18447
|
+
return null;
|
|
18448
|
+
}
|
|
18449
|
+
}
|
|
18450
|
+
function doctorCommand() {
|
|
18451
|
+
return new Command3("doctor").description("Diagnose why the proxy isn't catching your traffic (read-only; exits non-zero if this shell is unprotected)").option("--offline", "Skip the plan-service lookup", false).action(async (opts) => {
|
|
18452
|
+
let problems = 0;
|
|
18453
|
+
const note = (s) => console.log(s);
|
|
18454
|
+
console.log(chalk3.bold.cyan("\n Pretense Doctor\n"));
|
|
18455
|
+
note(chalk3.bold(" Proxy"));
|
|
18456
|
+
const record = readProxyRecord(pidFilePath());
|
|
18457
|
+
let recordedPort = null;
|
|
18458
|
+
if (record) {
|
|
18459
|
+
if (isProcessAlive(record.pid)) {
|
|
18460
|
+
const probe = await probePort(record.port);
|
|
18461
|
+
if (probe.isPretense) {
|
|
18462
|
+
recordedPort = record.port;
|
|
18463
|
+
const degraded = probe.status === "degraded" || probe.auditAvailable === false;
|
|
18464
|
+
note(chalk3.green(` \u2713 Pretense proxy running on :${record.port} (pid ${record.pid}).`));
|
|
18465
|
+
if (degraded) {
|
|
18466
|
+
note(chalk3.yellow(` \u26A0 Audit store unavailable \u2014 requests are still filtered, but ` + chalk3.cyan("pretense audit") + chalk3.yellow(" will be empty this session.")));
|
|
18467
|
+
}
|
|
18468
|
+
} else if (probe.alive) {
|
|
18469
|
+
problems += 1;
|
|
18470
|
+
note(chalk3.yellow(` \u26A0 Pidfile says a proxy is on :${record.port} (pid ${record.pid}), but that port is answering as something ELSE.`));
|
|
18471
|
+
note(chalk3.dim(` Something took the port. Stop the stray process, then ` + chalk3.cyan("pretense start") + chalk3.dim(".")));
|
|
18472
|
+
} else {
|
|
18473
|
+
problems += 1;
|
|
18474
|
+
note(chalk3.yellow(` \u26A0 Pidfile records pid ${record.pid} on :${record.port}, but it is not answering /health.`));
|
|
18475
|
+
note(chalk3.dim(" It may be starting, wedged, or dead. Try ") + chalk3.cyan("pretense stop") + chalk3.dim(" then ") + chalk3.cyan("pretense start") + chalk3.dim("."));
|
|
18476
|
+
}
|
|
18477
|
+
} else {
|
|
18478
|
+
problems += 1;
|
|
18479
|
+
note(chalk3.yellow(` \u26A0 Stale pidfile: pid ${record.pid} (port ${record.port}) is no longer alive.`));
|
|
18480
|
+
note(chalk3.dim(" Harmless, cleared on the next ") + chalk3.cyan("pretense start") + chalk3.dim("."));
|
|
18481
|
+
}
|
|
18482
|
+
} else {
|
|
18483
|
+
note(chalk3.dim(" \u25CB No Pretense proxy recorded (pidfile absent)."));
|
|
18484
|
+
}
|
|
18485
|
+
const stray = [];
|
|
18486
|
+
for (const p of COMMON_PORTS) {
|
|
18487
|
+
if (p === recordedPort) continue;
|
|
18488
|
+
const probe = await probePort(p, 400);
|
|
18489
|
+
if (probe.isPretense) stray.push(p);
|
|
18490
|
+
}
|
|
18491
|
+
if (stray.length > 0) {
|
|
18492
|
+
note(chalk3.yellow(` \u26A0 Other Pretense proxies answering on: ${stray.map((p) => `:${p}`).join(", ")}.`));
|
|
18493
|
+
note(chalk3.dim(" Multiple proxies can send your tool to the wrong one. ") + chalk3.cyan("pretense stop") + chalk3.dim(" clears the recorded one; stop extras by pid."));
|
|
18494
|
+
}
|
|
18495
|
+
note(chalk3.bold("\n This shell"));
|
|
18496
|
+
const setEnvs = BASE_URL_ENVS.filter((e) => (process.env[e.name] ?? "").trim() !== "");
|
|
18497
|
+
let protectedHere = false;
|
|
18498
|
+
if (setEnvs.length === 0) {
|
|
18499
|
+
problems += 1;
|
|
18500
|
+
note(chalk3.red(" \u2717 No base-URL env var is set in THIS terminal \u2014 your AI tool here is NOT protected."));
|
|
18501
|
+
note(chalk3.dim(" Env vars are per-shell: a proxy running elsewhere does not protect this terminal."));
|
|
18502
|
+
note(chalk3.dim(" Simplest fix (no export, no second terminal):"));
|
|
18503
|
+
note(" " + chalk3.cyan('pretense run claude "\u2026"') + chalk3.dim(" # or cursor, gemini, any tool"));
|
|
18504
|
+
if (recordedPort !== null) {
|
|
18505
|
+
note(chalk3.dim(" Or, IN THIS terminal:"));
|
|
18506
|
+
note(" " + chalk3.cyan(`export ANTHROPIC_BASE_URL=http://localhost:${recordedPort}`));
|
|
18507
|
+
}
|
|
18508
|
+
} else {
|
|
18509
|
+
for (const e of setEnvs) {
|
|
18510
|
+
const val = (process.env[e.name] ?? "").trim();
|
|
18511
|
+
const port = portFromBaseUrl(val);
|
|
18512
|
+
if (port === null) {
|
|
18513
|
+
problems += 1;
|
|
18514
|
+
note(chalk3.red(` \u2717 ${e.name}=${val} is not a valid URL.`));
|
|
18515
|
+
continue;
|
|
18516
|
+
}
|
|
18517
|
+
const probe = await probePort(port);
|
|
18518
|
+
if (probe.isPretense) {
|
|
18519
|
+
protectedHere = true;
|
|
18520
|
+
note(chalk3.green(` \u2713 ${e.name} \u2192 :${port} is a live Pretense proxy (${e.provider}).`));
|
|
18521
|
+
} else if (probe.alive) {
|
|
18522
|
+
problems += 1;
|
|
18523
|
+
note(chalk3.red(` \u2717 ${e.name} \u2192 :${port} is answering, but it is NOT a Pretense proxy.`));
|
|
18524
|
+
note(chalk3.dim(" Your traffic is going somewhere, but not through the firewall. Fix the URL or use ") + chalk3.cyan("pretense run") + chalk3.dim("."));
|
|
18525
|
+
} else {
|
|
18526
|
+
problems += 1;
|
|
18527
|
+
note(chalk3.red(` \u2717 ${e.name} \u2192 :${port} is set, but nothing is listening there.`));
|
|
18528
|
+
note(chalk3.dim(" Start the proxy (") + chalk3.cyan("pretense start") + chalk3.dim(") or, simplest, use ") + chalk3.cyan("pretense run") + chalk3.dim("."));
|
|
18529
|
+
}
|
|
18530
|
+
}
|
|
18531
|
+
}
|
|
18532
|
+
note(chalk3.bold("\n Plan"));
|
|
18533
|
+
try {
|
|
18534
|
+
const resolution = await resolvePlanTier({ offline: opts.offline });
|
|
18535
|
+
note(chalk3.dim(` Plan: ${tierLabel(resolution.tier)}`));
|
|
18536
|
+
for (const line of planMismatchHint(resolution)) {
|
|
18537
|
+
note(chalk3.dim(` ${line}`));
|
|
18538
|
+
}
|
|
18539
|
+
} catch {
|
|
18540
|
+
note(chalk3.dim(" Plan: could not be resolved (offline or no credentials)."));
|
|
18541
|
+
}
|
|
18542
|
+
if (protectedHere && problems === 0) {
|
|
18543
|
+
note(chalk3.bold.green("\n \u2713 This shell is protected \u2014 traffic routes through Pretense.\n"));
|
|
18544
|
+
process.exitCode = 0;
|
|
18545
|
+
} else if (protectedHere) {
|
|
18546
|
+
note(chalk3.bold.yellow(`
|
|
18547
|
+
\u26A0 This shell is protected, but doctor found ${problems} thing(s) to look at above.
|
|
18548
|
+
`));
|
|
18549
|
+
process.exitCode = 1;
|
|
18550
|
+
} else {
|
|
18551
|
+
note(chalk3.bold.red("\n \u2717 This shell is NOT protected. Use ") + chalk3.cyan("pretense run <tool>") + chalk3.bold.red(" \u2014 it wires everything for you.\n"));
|
|
18552
|
+
process.exitCode = 1;
|
|
18553
|
+
}
|
|
18554
|
+
});
|
|
18555
|
+
}
|
|
18556
|
+
|
|
18557
|
+
// src/commands/stop.ts
|
|
18558
|
+
function pidListeningOn2(port) {
|
|
18559
|
+
try {
|
|
18560
|
+
const out = execSync2(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, {
|
|
18561
|
+
encoding: "utf-8",
|
|
18562
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
18563
|
+
}).trim();
|
|
18564
|
+
const pid = parseInt(out.split("\n")[0] ?? "", 10);
|
|
18565
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
18566
|
+
} catch {
|
|
18567
|
+
return null;
|
|
18568
|
+
}
|
|
18569
|
+
}
|
|
18570
|
+
async function isRecordOurs(record) {
|
|
18571
|
+
const port = record.port;
|
|
18572
|
+
if (port > 0) {
|
|
18573
|
+
const probe = await probePort(port, 700);
|
|
18574
|
+
const listener = pidListeningOn2(port);
|
|
18575
|
+
if (probe.isPretense) {
|
|
18576
|
+
return listener === null || listener === record.pid;
|
|
18577
|
+
}
|
|
18578
|
+
return listener !== null && listener === record.pid;
|
|
18579
|
+
}
|
|
18580
|
+
return true;
|
|
18581
|
+
}
|
|
18582
|
+
function stopCommand() {
|
|
18583
|
+
return new Command4("stop").description("Stop the running Pretense proxy").option("--json", "Machine-readable outcome", false).action(async (opts) => {
|
|
18584
|
+
const path2 = pidFilePath();
|
|
18585
|
+
const record = readProxyRecord(path2);
|
|
18586
|
+
if (record && isProcessAlive(record.pid) && !await isRecordOurs(record)) {
|
|
18587
|
+
removeProxyRecord(path2);
|
|
18588
|
+
const outcome2 = { kind: "foreign", pid: record.pid, port: record.port };
|
|
18589
|
+
if (opts.json) {
|
|
18590
|
+
console.log(JSON.stringify(outcome2, null, 2));
|
|
18591
|
+
process.exit(0);
|
|
18592
|
+
}
|
|
18593
|
+
console.log(
|
|
18594
|
+
chalk4.yellow(`
|
|
18595
|
+
\xB7 Not stopping pid ${record.pid}: it is not the Pretense proxy.`) + chalk4.dim(`
|
|
18596
|
+
The recorded proxy (port ${record.port}) is gone and that pid now`) + chalk4.dim(`
|
|
18597
|
+
belongs to an unrelated process \u2014 Pretense will not signal it.`) + chalk4.dim(`
|
|
18598
|
+
Cleaned up the stale record; nothing was killed.
|
|
18599
|
+
`)
|
|
18600
|
+
);
|
|
18601
|
+
process.exit(0);
|
|
18602
|
+
}
|
|
18603
|
+
const outcome = await stopProxy(path2);
|
|
18604
|
+
if (opts.json) {
|
|
18605
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
18606
|
+
process.exit(outcome.kind === "refused" || outcome.kind === "not-permitted" ? 1 : 0);
|
|
18607
|
+
}
|
|
18608
|
+
switch (outcome.kind) {
|
|
18609
|
+
case "not-running":
|
|
18610
|
+
console.log(
|
|
18611
|
+
chalk4.dim(`
|
|
18612
|
+
\xB7 No running Pretense proxy recorded (${path2} does not exist).`) + chalk4.dim(`
|
|
18613
|
+
Start one with `) + chalk4.cyan("pretense start") + "\n"
|
|
18614
|
+
);
|
|
18615
|
+
process.exit(0);
|
|
18616
|
+
break;
|
|
18617
|
+
case "stale":
|
|
18618
|
+
console.log(
|
|
18619
|
+
chalk4.yellow(
|
|
18620
|
+
`
|
|
18621
|
+
\xB7 Stale PID file cleaned up${outcome.pid ? ` (pid ${outcome.pid} is not running)` : ""}.`
|
|
18622
|
+
) + chalk4.dim(`
|
|
18623
|
+
No proxy was running.
|
|
18624
|
+
`)
|
|
18625
|
+
);
|
|
18626
|
+
process.exit(0);
|
|
18627
|
+
break;
|
|
18628
|
+
case "stopped":
|
|
18629
|
+
console.log(
|
|
18630
|
+
chalk4.green(
|
|
18631
|
+
`
|
|
18632
|
+
\u2713 Pretense proxy stopped (pid ${outcome.pid}${outcome.port ? `, port ${outcome.port}` : ""})` + (outcome.forced ? " \u2014 did not exit on SIGTERM, sent SIGKILL" : "") + "\n"
|
|
18633
|
+
)
|
|
18634
|
+
);
|
|
18635
|
+
process.exit(0);
|
|
18636
|
+
break;
|
|
18637
|
+
case "not-permitted":
|
|
18638
|
+
console.error(
|
|
18639
|
+
chalk4.red(`
|
|
18640
|
+
x Cannot stop pid ${outcome.pid}: it belongs to another user.`) + chalk4.dim(`
|
|
18641
|
+
The proxy is still running. Stop it as its owner, or: sudo kill ${outcome.pid}
|
|
18642
|
+
`)
|
|
18643
|
+
);
|
|
18644
|
+
process.exit(1);
|
|
18645
|
+
break;
|
|
18646
|
+
case "refused":
|
|
18647
|
+
console.error(
|
|
18648
|
+
chalk4.red(`
|
|
18649
|
+
x Pid ${outcome.pid} is still alive after SIGTERM and SIGKILL.`) + chalk4.dim(`
|
|
18650
|
+
${path2} was left in place so you can investigate: ps -p ${outcome.pid}
|
|
18651
|
+
`)
|
|
18652
|
+
);
|
|
18653
|
+
process.exit(1);
|
|
18654
|
+
break;
|
|
18655
|
+
}
|
|
18656
|
+
});
|
|
18657
|
+
}
|
|
18658
|
+
|
|
18659
|
+
// src/commands/scan.ts
|
|
18660
|
+
init_esm_shims();
|
|
18661
|
+
import { Command as Command5, Option, InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
18662
|
+
import chalk5 from "chalk";
|
|
18663
|
+
import { readFileSync as readFileSync8, statSync as statSync3, readdirSync as readdirSync2, openSync, readSync, closeSync } from "fs";
|
|
18664
|
+
import { execSync as execSync3 } from "child_process";
|
|
18665
|
+
import { resolve as resolve2, join as join8, extname as extname2, basename } from "path";
|
|
18666
|
+
|
|
18667
|
+
// src/safe-output.ts
|
|
18668
|
+
init_esm_shims();
|
|
18669
|
+
var UNSAFE_VALUES_FLAG = "--unsafe-show-values";
|
|
18670
|
+
function lineColFromOffset2(text, offset) {
|
|
18671
|
+
const clamped = Math.max(0, Math.min(offset, text.length));
|
|
18672
|
+
let line = 1;
|
|
18673
|
+
let lineStart = 0;
|
|
18674
|
+
for (let i = 0; i < clamped; i++) {
|
|
18675
|
+
if (text.charCodeAt(i) === 10) {
|
|
18676
|
+
line++;
|
|
18677
|
+
lineStart = i + 1;
|
|
18678
|
+
}
|
|
18679
|
+
}
|
|
18680
|
+
return { line, column: clamped - lineStart + 1 };
|
|
18681
|
+
}
|
|
18682
|
+
function withLocations(result, content) {
|
|
18683
|
+
const locate = (m) => lineColFromOffset2(content, m.start);
|
|
18684
|
+
return {
|
|
18685
|
+
...result,
|
|
18686
|
+
matches: result.matches.map((m) => ({ ...m, ...locate(m) }))
|
|
18687
|
+
};
|
|
18688
|
+
}
|
|
18689
|
+
function safeLocator(m) {
|
|
18690
|
+
const len = Math.max(0, m.end - m.start);
|
|
18691
|
+
if (m.deobfuscated && len === 0) return "len=0 unmapped";
|
|
18692
|
+
const pos = m.line !== void 0 && m.column !== void 0 ? `L${m.line}:${m.column} ` : "";
|
|
18693
|
+
return `${pos}len=${len}${m.deobfuscated ? " enc" : ""}`;
|
|
18694
|
+
}
|
|
18695
|
+
var VALUE_FIELDS = ["value", "masked", "deobfuscatedValue"];
|
|
18696
|
+
function stripRow(row) {
|
|
18697
|
+
const out = {};
|
|
18698
|
+
for (const [k, v] of Object.entries(row)) {
|
|
18699
|
+
if (VALUE_FIELDS.includes(k)) continue;
|
|
18700
|
+
out[k] = v;
|
|
18701
|
+
}
|
|
18702
|
+
out.length = typeof row.end === "number" && typeof row.start === "number" ? row.end - row.start : 0;
|
|
18703
|
+
return out;
|
|
18704
|
+
}
|
|
18705
|
+
function stripValues(result) {
|
|
18266
18706
|
const r = result;
|
|
18267
18707
|
return {
|
|
18268
18708
|
...r,
|
|
@@ -18404,7 +18844,7 @@ function walkDir(dir) {
|
|
|
18404
18844
|
} catch (err) {
|
|
18405
18845
|
if (isSkippableWalkError(err)) {
|
|
18406
18846
|
const code = err.code;
|
|
18407
|
-
console.error(
|
|
18847
|
+
console.error(chalk5.yellow(` Skipping unreadable path (${code}): ${dir}`));
|
|
18408
18848
|
return files;
|
|
18409
18849
|
}
|
|
18410
18850
|
throw err;
|
|
@@ -18412,16 +18852,16 @@ function walkDir(dir) {
|
|
|
18412
18852
|
for (const entry of entries) {
|
|
18413
18853
|
if (entry.isDirectory()) {
|
|
18414
18854
|
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
|
|
18415
|
-
files.push(...walkDir(
|
|
18855
|
+
files.push(...walkDir(join8(dir, entry.name)));
|
|
18416
18856
|
} else if (entry.isFile()) {
|
|
18417
|
-
files.push(
|
|
18857
|
+
files.push(join8(dir, entry.name));
|
|
18418
18858
|
}
|
|
18419
18859
|
}
|
|
18420
18860
|
return files;
|
|
18421
18861
|
}
|
|
18422
18862
|
function gitExecChecked(cmd) {
|
|
18423
18863
|
try {
|
|
18424
|
-
const out =
|
|
18864
|
+
const out = execSync3(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
18425
18865
|
return { ok: true, lines: out ? out.split("\n").filter(Boolean) : [] };
|
|
18426
18866
|
} catch (err) {
|
|
18427
18867
|
const e = err;
|
|
@@ -18468,7 +18908,7 @@ function warnIfLargeForL2(byteLength, filePath) {
|
|
|
18468
18908
|
const mb = (byteLength / (1024 * 1024)).toFixed(1);
|
|
18469
18909
|
const where = filePath ? ` \u2014 ${basename(filePath)}` : "";
|
|
18470
18910
|
console.error(
|
|
18471
|
-
|
|
18911
|
+
chalk5.yellow(
|
|
18472
18912
|
` Note: L2 AST scanning is slow on very large files (${mb}MB${where}); this may take a while.`
|
|
18473
18913
|
)
|
|
18474
18914
|
);
|
|
@@ -18531,14 +18971,14 @@ async function scanContentRaw(content, opts, filePath) {
|
|
|
18531
18971
|
return result;
|
|
18532
18972
|
}
|
|
18533
18973
|
async function scanFile(filePath, opts) {
|
|
18534
|
-
const content =
|
|
18974
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
18535
18975
|
return scanContent(content, opts, filePath);
|
|
18536
18976
|
}
|
|
18537
18977
|
function severityIcon(severity) {
|
|
18538
18978
|
return severity === "critical" ? "\u{1F534}" : severity === "high" ? "\u{1F7E0}" : severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
|
|
18539
18979
|
}
|
|
18540
18980
|
function levelTag(level) {
|
|
18541
|
-
return level === 2 ?
|
|
18981
|
+
return level === 2 ? chalk5.magenta("L2") : chalk5.blue("L1");
|
|
18542
18982
|
}
|
|
18543
18983
|
function rowTail(match3, opts) {
|
|
18544
18984
|
if (opts.unsafeShowValues) return match3.masked;
|
|
@@ -18549,12 +18989,12 @@ function emitResult(result, opts) {
|
|
|
18549
18989
|
}
|
|
18550
18990
|
function printFindings(filePath, result, opts) {
|
|
18551
18991
|
if (result.clean) return;
|
|
18552
|
-
console.log(
|
|
18992
|
+
console.log(chalk5.underline(`
|
|
18553
18993
|
${filePath}`));
|
|
18554
18994
|
for (const match3 of result.matches) {
|
|
18555
|
-
const actionColor = match3.action === "block" ?
|
|
18995
|
+
const actionColor = match3.action === "block" ? chalk5.red : match3.action === "redact" ? chalk5.yellow : chalk5.dim;
|
|
18556
18996
|
console.log(
|
|
18557
|
-
` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${
|
|
18997
|
+
` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk5.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk5.dim(rowTail(match3, opts))}`
|
|
18558
18998
|
);
|
|
18559
18999
|
}
|
|
18560
19000
|
}
|
|
@@ -18640,7 +19080,7 @@ async function scanFiles(files, opts) {
|
|
|
18640
19080
|
let blocked = 0;
|
|
18641
19081
|
for (const f of files) {
|
|
18642
19082
|
if (isBinaryFile(f)) {
|
|
18643
|
-
console.error(
|
|
19083
|
+
console.error(chalk5.dim(` Skipping binary file: ${f}`));
|
|
18644
19084
|
continue;
|
|
18645
19085
|
}
|
|
18646
19086
|
try {
|
|
@@ -18682,17 +19122,17 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
|
|
|
18682
19122
|
}
|
|
18683
19123
|
if (files.length === 0) {
|
|
18684
19124
|
console.error(
|
|
18685
|
-
|
|
18686
|
-
No supported files found in ${label}.`) +
|
|
19125
|
+
chalk5.yellow(`
|
|
19126
|
+
No supported files found in ${label}.`) + chalk5.dim(`
|
|
18687
19127
|
Supported: ${SUPPORTED_EXTENSIONS_DISPLAY}
|
|
18688
19128
|
`)
|
|
18689
19129
|
);
|
|
18690
19130
|
} else {
|
|
18691
19131
|
console.log(
|
|
18692
|
-
|
|
18693
|
-
No supported source files found.`) +
|
|
19132
|
+
chalk5.yellow(`
|
|
19133
|
+
No supported source files found.`) + chalk5.dim(`
|
|
18694
19134
|
Currently supports: TypeScript, JavaScript, Python, Go, Java, C#, Ruby, Rust
|
|
18695
|
-
`) +
|
|
19135
|
+
`) + chalk5.dim(` (${SUPPORTED_EXTENSIONS_DISPLAY})
|
|
18696
19136
|
`)
|
|
18697
19137
|
);
|
|
18698
19138
|
}
|
|
@@ -18723,18 +19163,18 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
|
|
|
18723
19163
|
if (allResults.length === 0) {
|
|
18724
19164
|
if (notice) console.log(notice);
|
|
18725
19165
|
if (policyLine) console.log(policyLine);
|
|
18726
|
-
console.log(
|
|
19166
|
+
console.log(chalk5.green(` \u2713 No secrets or PII detected`));
|
|
18727
19167
|
process.exit(0);
|
|
18728
19168
|
}
|
|
18729
|
-
console.log(
|
|
19169
|
+
console.log(chalk5.cyan(`
|
|
18730
19170
|
Pretense Scan \u2014 ${label}
|
|
18731
19171
|
`));
|
|
18732
|
-
console.log(
|
|
19172
|
+
console.log(chalk5.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)}
|
|
18733
19173
|
`));
|
|
18734
19174
|
if (policyLine) console.log(policyLine + "\n");
|
|
18735
19175
|
if (notice) console.log(notice + "\n");
|
|
18736
19176
|
for (const { file, result } of allResults) printFindings(file, result, opts);
|
|
18737
|
-
console.log(
|
|
19177
|
+
console.log(chalk5.dim(`
|
|
18738
19178
|
Total: ${total} findings | ${blocked} blocked
|
|
18739
19179
|
`));
|
|
18740
19180
|
process.exit(findingsExitCode);
|
|
@@ -18764,7 +19204,7 @@ function levelOption() {
|
|
|
18764
19204
|
function parseLevel(v) {
|
|
18765
19205
|
if (v === "1") return 1;
|
|
18766
19206
|
if (v === "2") return 2;
|
|
18767
|
-
throw new
|
|
19207
|
+
throw new InvalidArgumentError2("Detection level must be 1 (regex) or 2 (AST).");
|
|
18768
19208
|
}
|
|
18769
19209
|
function levelsLabel(opts) {
|
|
18770
19210
|
if (opts.level === 1) return "L1 only (--level 1)";
|
|
@@ -18776,13 +19216,13 @@ function l2Notice(results, opts) {
|
|
|
18776
19216
|
if (isAstCoreAvailable()) {
|
|
18777
19217
|
const errored = results.filter((r) => r.result.l2?.parseErrors).length;
|
|
18778
19218
|
if (errored > 0) {
|
|
18779
|
-
return
|
|
19219
|
+
return chalk5.yellow(
|
|
18780
19220
|
` ! L2 parsed ${errored} file(s) with syntax errors \u2014 AST findings may be incomplete. L1 results are unaffected.`
|
|
18781
19221
|
);
|
|
18782
19222
|
}
|
|
18783
19223
|
return null;
|
|
18784
19224
|
}
|
|
18785
|
-
return
|
|
19225
|
+
return chalk5.yellow(
|
|
18786
19226
|
" ! L2 unavailable \u2014 the native AST addon (@pretense/scanner-rs) is not installed for this platform.\n Scanned with L1 (regex) only. All L1 findings are reported in full; no finding was dropped."
|
|
18787
19227
|
);
|
|
18788
19228
|
}
|
|
@@ -18794,9 +19234,9 @@ function validateScanFlags(opts) {
|
|
|
18794
19234
|
const raw2 = String(opts.format).trim().toLowerCase();
|
|
18795
19235
|
if (!VALID_FORMATS.includes(raw2)) {
|
|
18796
19236
|
console.error(
|
|
18797
|
-
|
|
19237
|
+
chalk5.red(`
|
|
18798
19238
|
x Unknown --format: "${opts.format}"
|
|
18799
|
-
`) +
|
|
19239
|
+
`) + chalk5.dim(` Valid formats: ${VALID_FORMATS.join(", ")}.
|
|
18800
19240
|
`)
|
|
18801
19241
|
);
|
|
18802
19242
|
process.exit(USAGE_EXIT);
|
|
@@ -18807,9 +19247,9 @@ function validateScanFlags(opts) {
|
|
|
18807
19247
|
const raw2 = String(opts.severity).trim().toLowerCase();
|
|
18808
19248
|
if (!VALID_SEVERITIES.includes(raw2)) {
|
|
18809
19249
|
console.error(
|
|
18810
|
-
|
|
19250
|
+
chalk5.red(`
|
|
18811
19251
|
x Unknown --severity: "${opts.severity}"
|
|
18812
|
-
`) +
|
|
19252
|
+
`) + chalk5.dim(` Valid severities: ${VALID_SEVERITIES.join(", ")}.
|
|
18813
19253
|
`)
|
|
18814
19254
|
);
|
|
18815
19255
|
process.exit(USAGE_EXIT);
|
|
@@ -18826,7 +19266,7 @@ function applyPolicy(opts) {
|
|
|
18826
19266
|
if (resolved.scannerPreset) opts.presetName = resolved.scannerPreset;
|
|
18827
19267
|
}
|
|
18828
19268
|
} catch (err) {
|
|
18829
|
-
console.error(
|
|
19269
|
+
console.error(chalk5.red(`
|
|
18830
19270
|
x ${err.message}
|
|
18831
19271
|
`));
|
|
18832
19272
|
process.exit(USAGE_EXIT);
|
|
@@ -18836,15 +19276,15 @@ function applyPolicy(opts) {
|
|
|
18836
19276
|
function policyNotice(opts) {
|
|
18837
19277
|
if (!opts.policyResolved) return null;
|
|
18838
19278
|
const p = opts.policyResolved;
|
|
18839
|
-
const line =
|
|
18840
|
-
return p.scannerPreset ? line :
|
|
19279
|
+
const line = chalk5.dim(` ${describePolicy(p)}`);
|
|
19280
|
+
return p.scannerPreset ? line : chalk5.yellow(` ! ${describePolicy(p)}`);
|
|
18841
19281
|
}
|
|
18842
19282
|
function inheritedScanOpts(cmd) {
|
|
18843
19283
|
return cmd.optsWithGlobals();
|
|
18844
19284
|
}
|
|
18845
19285
|
function ciSubcommand() {
|
|
18846
19286
|
return addScanOpts(
|
|
18847
|
-
new
|
|
19287
|
+
new Command5("ci").description("CI-optimized scan (non-interactive, exit code 0=clean 2=findings)").argument("[path]", "File or directory to scan (default: all git-tracked files)").allowExcessArguments(false)
|
|
18848
19288
|
).action(async (target, opts, cmd) => {
|
|
18849
19289
|
opts = inheritedScanOpts(cmd);
|
|
18850
19290
|
applyPolicy(opts);
|
|
@@ -18852,8 +19292,8 @@ function ciSubcommand() {
|
|
|
18852
19292
|
const files2 = collectFiles(target);
|
|
18853
19293
|
if (files2.length === 0) {
|
|
18854
19294
|
console.error(
|
|
18855
|
-
|
|
18856
|
-
x Path not found or empty: ${target}`) +
|
|
19295
|
+
chalk5.red(`
|
|
19296
|
+
x Path not found or empty: ${target}`) + chalk5.dim(`
|
|
18857
19297
|
Try: pretense scan ci <file-or-directory>, or omit the path to scan git-tracked files.
|
|
18858
19298
|
`)
|
|
18859
19299
|
);
|
|
@@ -18865,11 +19305,11 @@ function ciSubcommand() {
|
|
|
18865
19305
|
const tracked = gitExecChecked("git ls-files");
|
|
18866
19306
|
if (!tracked.ok) {
|
|
18867
19307
|
console.error(
|
|
18868
|
-
|
|
18869
|
-
x scan ci could not enumerate git-tracked files \u2014 NOTHING WAS SCANNED.`) +
|
|
18870
|
-
git: ${tracked.error}`) +
|
|
18871
|
-
This is a scan FAILURE (exit 1), not a clean result. Reporting 0 here`) +
|
|
18872
|
-
would mark a CI gate green over a tree that was never read.`) +
|
|
19308
|
+
chalk5.red(`
|
|
19309
|
+
x scan ci could not enumerate git-tracked files \u2014 NOTHING WAS SCANNED.`) + chalk5.dim(`
|
|
19310
|
+
git: ${tracked.error}`) + chalk5.dim(`
|
|
19311
|
+
This is a scan FAILURE (exit 1), not a clean result. Reporting 0 here`) + chalk5.dim(`
|
|
19312
|
+
would mark a CI gate green over a tree that was never read.`) + chalk5.dim(`
|
|
18873
19313
|
Fix the checkout, or scan a path directly: pretense scan ci <path>
|
|
18874
19314
|
`)
|
|
18875
19315
|
);
|
|
@@ -18885,7 +19325,7 @@ function ciSubcommand() {
|
|
|
18885
19325
|
}
|
|
18886
19326
|
function commitRangeSubcommand() {
|
|
18887
19327
|
return addScanOpts(
|
|
18888
|
-
new
|
|
19328
|
+
new Command5("commit-range").description("Scan files changed between two commits").argument("<range>", "Commit range (e.g. base..head)")
|
|
18889
19329
|
).action(async (range, opts, cmd) => {
|
|
18890
19330
|
opts = inheritedScanOpts(cmd);
|
|
18891
19331
|
applyPolicy(opts);
|
|
@@ -18893,7 +19333,7 @@ function commitRangeSubcommand() {
|
|
|
18893
19333
|
if (files.length === 0) {
|
|
18894
19334
|
const fmt2 = resolveFormat(opts);
|
|
18895
19335
|
if (fmt2 === "json") console.log(JSON.stringify({ clean: true, files: 0, findings: 0 }));
|
|
18896
|
-
else console.log(
|
|
19336
|
+
else console.log(chalk5.green(" \u2713 No changed files in range"));
|
|
18897
19337
|
process.exit(0);
|
|
18898
19338
|
}
|
|
18899
19339
|
await outputMultiFileResults(`commit-range ${range}`, files, opts);
|
|
@@ -18901,7 +19341,7 @@ function commitRangeSubcommand() {
|
|
|
18901
19341
|
}
|
|
18902
19342
|
function preCommitSubcommand() {
|
|
18903
19343
|
return addScanOpts(
|
|
18904
|
-
new
|
|
19344
|
+
new Command5("pre-commit").description("Scan git staged files")
|
|
18905
19345
|
).action(async (opts, cmd) => {
|
|
18906
19346
|
opts = inheritedScanOpts(cmd);
|
|
18907
19347
|
applyPolicy(opts);
|
|
@@ -18909,23 +19349,79 @@ function preCommitSubcommand() {
|
|
|
18909
19349
|
if (files.length === 0) {
|
|
18910
19350
|
const fmt2 = resolveFormat(opts);
|
|
18911
19351
|
if (fmt2 === "json") console.log(JSON.stringify({ clean: true, files: 0, findings: 0 }));
|
|
18912
|
-
else console.log(
|
|
19352
|
+
else console.log(chalk5.green(" \u2713 No staged files to scan"));
|
|
18913
19353
|
process.exit(0);
|
|
18914
19354
|
}
|
|
18915
19355
|
await outputMultiFileResults(`pre-commit (${files.length} staged files)`, files, opts);
|
|
18916
19356
|
});
|
|
18917
19357
|
}
|
|
19358
|
+
function isZeroOid(oid) {
|
|
19359
|
+
return /^0+$/.test(oid.trim());
|
|
19360
|
+
}
|
|
19361
|
+
function emptyTreeOid() {
|
|
19362
|
+
const r = gitExecChecked("git hash-object -t tree /dev/null");
|
|
19363
|
+
return r.ok && r.lines[0] ? r.lines[0] : "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
19364
|
+
}
|
|
19365
|
+
function changedFilesForPush(localSha, remoteSha) {
|
|
19366
|
+
if (isZeroOid(localSha)) return { ok: true, lines: [] };
|
|
19367
|
+
if (!isZeroOid(remoteSha)) {
|
|
19368
|
+
return gitExecChecked(`git diff --name-only ${remoteSha} ${localSha}`);
|
|
19369
|
+
}
|
|
19370
|
+
const revs = gitExecChecked(`git rev-list ${localSha} --not --remotes`);
|
|
19371
|
+
if (revs.ok && revs.lines.length > 0) {
|
|
19372
|
+
const oldest = revs.lines[revs.lines.length - 1];
|
|
19373
|
+
const parent = gitExecChecked(`git rev-parse --verify --quiet ${oldest}^`);
|
|
19374
|
+
const base = parent.ok && parent.lines[0] ? parent.lines[0] : emptyTreeOid();
|
|
19375
|
+
return gitExecChecked(`git diff --name-only ${base} ${localSha}`);
|
|
19376
|
+
}
|
|
19377
|
+
if (!revs.ok) return revs;
|
|
19378
|
+
return gitExecChecked(`git diff --name-only ${emptyTreeOid()} ${localSha}`);
|
|
19379
|
+
}
|
|
19380
|
+
async function readStdin() {
|
|
19381
|
+
if (process.stdin.isTTY) return "";
|
|
19382
|
+
const chunks = [];
|
|
19383
|
+
try {
|
|
19384
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
19385
|
+
} catch {
|
|
19386
|
+
return "";
|
|
19387
|
+
}
|
|
19388
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
19389
|
+
}
|
|
18918
19390
|
function prePushSubcommand() {
|
|
18919
19391
|
return addScanOpts(
|
|
18920
|
-
new
|
|
19392
|
+
new Command5("pre-push").description("Scan the commits being pushed (reads git's pre-push ref lines on stdin)")
|
|
18921
19393
|
).action(async (opts, cmd) => {
|
|
18922
19394
|
opts = inheritedScanOpts(cmd);
|
|
18923
19395
|
applyPolicy(opts);
|
|
18924
|
-
const
|
|
19396
|
+
const stdin = await readStdin();
|
|
19397
|
+
const refLines = stdin.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
19398
|
+
const changed = /* @__PURE__ */ new Set();
|
|
19399
|
+
if (refLines.length > 0) {
|
|
19400
|
+
for (const line of refLines) {
|
|
19401
|
+
const [, localSha, , remoteSha] = line.split(/\s+/);
|
|
19402
|
+
if (!localSha || !remoteSha) continue;
|
|
19403
|
+
const r = changedFilesForPush(localSha, remoteSha);
|
|
19404
|
+
if (!r.ok) {
|
|
19405
|
+
console.error(
|
|
19406
|
+
chalk5.red(`
|
|
19407
|
+
x pre-push: could not determine what is being pushed \u2014 NOTHING WAS SCANNED.`) + chalk5.dim(`
|
|
19408
|
+
git: ${r.error}`) + chalk5.dim(`
|
|
19409
|
+
Blocking the push (exit 1) rather than letting an unscanned commit through.`) + chalk5.dim(`
|
|
19410
|
+
Override at your own risk with: git push --no-verify
|
|
19411
|
+
`)
|
|
19412
|
+
);
|
|
19413
|
+
process.exit(1);
|
|
19414
|
+
}
|
|
19415
|
+
for (const f of r.lines) changed.add(f);
|
|
19416
|
+
}
|
|
19417
|
+
} else {
|
|
19418
|
+
for (const f of gitExec("git diff --name-only @{push}..HEAD")) changed.add(f);
|
|
19419
|
+
}
|
|
19420
|
+
const files = filterExisting([...changed]);
|
|
18925
19421
|
if (files.length === 0) {
|
|
18926
19422
|
const fmt2 = resolveFormat(opts);
|
|
18927
19423
|
if (fmt2 === "json") console.log(JSON.stringify({ clean: true, files: 0, findings: 0 }));
|
|
18928
|
-
else console.log(
|
|
19424
|
+
else console.log(chalk5.green(" \u2713 No changed files to push"));
|
|
18929
19425
|
process.exit(0);
|
|
18930
19426
|
}
|
|
18931
19427
|
await outputMultiFileResults(`pre-push (${files.length} changed files)`, files, opts);
|
|
@@ -18933,15 +19429,15 @@ function prePushSubcommand() {
|
|
|
18933
19429
|
}
|
|
18934
19430
|
function pathSubcommand() {
|
|
18935
19431
|
return addScanOpts(
|
|
18936
|
-
new
|
|
19432
|
+
new Command5("path").description("Scan a file or directory (recursive)").argument("<target>", "File or directory to scan")
|
|
18937
19433
|
).action(async (target, opts, cmd) => {
|
|
18938
19434
|
opts = inheritedScanOpts(cmd);
|
|
18939
19435
|
applyPolicy(opts);
|
|
18940
19436
|
const files = collectFiles(target);
|
|
18941
19437
|
if (files.length === 0) {
|
|
18942
19438
|
console.error(
|
|
18943
|
-
|
|
18944
|
-
x Path not found or empty: ${target}`) +
|
|
19439
|
+
chalk5.red(`
|
|
19440
|
+
x Path not found or empty: ${target}`) + chalk5.dim(`
|
|
18945
19441
|
Try: pretense scan path <file-or-directory>
|
|
18946
19442
|
`)
|
|
18947
19443
|
);
|
|
@@ -18951,7 +19447,7 @@ function pathSubcommand() {
|
|
|
18951
19447
|
});
|
|
18952
19448
|
}
|
|
18953
19449
|
function scanCommand() {
|
|
18954
|
-
const cmd = new
|
|
19450
|
+
const cmd = new Command5("scan").description("Scan for secrets and PII (file, directory, or git-aware modes)").argument("[file]", "File to scan (omit to read from stdin)").allowExcessArguments(false).option("--json", "Output results as JSON (alias for --format json)", false).option("--format <fmt>", "Output format: text, json, sarif, csv (default: text)").option("--severity <lvl>", "Minimum severity to report: low, medium, high, critical").option("--no-entropy", "Disable entropy-based detection", false).option(
|
|
18955
19451
|
"--policy <preset>",
|
|
18956
19452
|
// A preset RE-WEIGHTS detector severity/action for that framework
|
|
18957
19453
|
// (see compliance-preset.ts `actionOverrides`). It does NOT narrow the
|
|
@@ -18974,8 +19470,8 @@ function scanCommand() {
|
|
|
18974
19470
|
const allFiles = collectFiles(file);
|
|
18975
19471
|
if (allFiles.length === 0) {
|
|
18976
19472
|
console.error(
|
|
18977
|
-
|
|
18978
|
-
No supported files found in ${file}.`) +
|
|
19473
|
+
chalk5.yellow(`
|
|
19474
|
+
No supported files found in ${file}.`) + chalk5.dim(`
|
|
18979
19475
|
Supported: ${SUPPORTED_EXTENSIONS_DISPLAY}
|
|
18980
19476
|
`)
|
|
18981
19477
|
);
|
|
@@ -18986,17 +19482,17 @@ function scanCommand() {
|
|
|
18986
19482
|
} catch {
|
|
18987
19483
|
}
|
|
18988
19484
|
if (isBinaryFile(file)) {
|
|
18989
|
-
console.error(
|
|
19485
|
+
console.error(chalk5.yellow(`
|
|
18990
19486
|
Skipping binary file: ${file}
|
|
18991
19487
|
`));
|
|
18992
19488
|
process.exit(0);
|
|
18993
19489
|
}
|
|
18994
19490
|
try {
|
|
18995
|
-
content =
|
|
19491
|
+
content = readFileSync8(file, "utf-8");
|
|
18996
19492
|
} catch {
|
|
18997
19493
|
console.error(
|
|
18998
|
-
|
|
18999
|
-
x Cannot read file: ${file}`) +
|
|
19494
|
+
chalk5.red(`
|
|
19495
|
+
x Cannot read file: ${file}`) + chalk5.dim(`
|
|
19000
19496
|
Try: check the file exists and you have read permission.
|
|
19001
19497
|
`)
|
|
19002
19498
|
);
|
|
@@ -19005,7 +19501,7 @@ function scanCommand() {
|
|
|
19005
19501
|
} else {
|
|
19006
19502
|
if (process.stdin.isTTY) {
|
|
19007
19503
|
console.error(
|
|
19008
|
-
|
|
19504
|
+
chalk5.yellow("\n x Nothing to scan.\n") + chalk5.dim(" Scan this project: ") + chalk5.cyan("pretense scan .") + "\n" + chalk5.dim(" Scan a path: ") + chalk5.cyan("pretense scan <file-or-dir>") + "\n" + chalk5.dim(" Or pipe content: ") + chalk5.cyan("cat file | pretense scan") + "\n"
|
|
19009
19505
|
);
|
|
19010
19506
|
process.exit(USAGE_EXIT);
|
|
19011
19507
|
}
|
|
@@ -19014,484 +19510,313 @@ function scanCommand() {
|
|
|
19014
19510
|
content = Buffer.concat(chunks).toString("utf-8");
|
|
19015
19511
|
if (content.trim() === "") {
|
|
19016
19512
|
console.error(
|
|
19017
|
-
|
|
19018
|
-
);
|
|
19019
|
-
process.exit(USAGE_EXIT);
|
|
19020
|
-
}
|
|
19021
|
-
}
|
|
19022
|
-
const rawResult = await scanContent(content, opts, file);
|
|
19023
|
-
const filteredMatches = filterBySeverity(rawResult.matches, opts?.severity);
|
|
19024
|
-
const result = { ...rawResult, matches: filteredMatches, clean: filteredMatches.length === 0 };
|
|
19025
|
-
const fmt2 = resolveFormat(opts ?? {});
|
|
19026
|
-
if (fmt2 === "json") {
|
|
19027
|
-
console.log(JSON.stringify(emitResult(result, opts ?? {}), null, 2));
|
|
19028
|
-
process.exit(result.clean ? 0 : 1);
|
|
19029
|
-
}
|
|
19030
|
-
if (fmt2 === "sarif") {
|
|
19031
|
-
const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
|
|
19032
|
-
console.log(JSON.stringify(toSarif(fileResults, opts ?? {}), null, 2));
|
|
19033
|
-
process.exit(result.clean ? 0 : 1);
|
|
19034
|
-
}
|
|
19035
|
-
if (fmt2 === "csv") {
|
|
19036
|
-
const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
|
|
19037
|
-
console.log(toCsv(fileResults, opts ?? {}));
|
|
19038
|
-
process.exit(result.clean ? 0 : 1);
|
|
19039
|
-
}
|
|
19040
|
-
console.log(chalk4.cyan(`
|
|
19041
|
-
Pretense Scan \u2014 ${file ?? "stdin"}
|
|
19042
|
-
`));
|
|
19043
|
-
console.log(chalk4.dim(` Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)} | Duration: ${result.durationMs}ms
|
|
19044
|
-
`));
|
|
19045
|
-
{
|
|
19046
|
-
const policyLine = policyNotice(opts);
|
|
19047
|
-
if (policyLine) console.log(policyLine + "\n");
|
|
19048
|
-
}
|
|
19049
|
-
if (opts.level !== 1 && result.l2 && !result.l2.ran && result.l2.reason) {
|
|
19050
|
-
console.log(chalk4.yellow(` ! ${result.l2.reason}`));
|
|
19051
|
-
console.log(chalk4.dim(" L1 (regex) ran in full; no finding was dropped.\n"));
|
|
19052
|
-
}
|
|
19053
|
-
if (result.clean) {
|
|
19054
|
-
console.log(chalk4.green(" \u2713 No secrets or PII detected\n"));
|
|
19055
|
-
process.exit(0);
|
|
19056
|
-
}
|
|
19057
|
-
for (const match3 of result.matches) {
|
|
19058
|
-
const actionColor = match3.action === "block" ? chalk4.red : match3.action === "redact" ? chalk4.yellow : chalk4.dim;
|
|
19059
|
-
console.log(
|
|
19060
|
-
` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk4.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk4.dim(rowTail(match3, opts ?? {}))}`
|
|
19061
|
-
);
|
|
19062
|
-
}
|
|
19063
|
-
const blocked = result.matches.filter((m) => m.action === "block").length;
|
|
19064
|
-
const redacted = result.matches.filter((m) => m.action === "redact").length;
|
|
19065
|
-
console.log(chalk4.dim(`
|
|
19066
|
-
Total: ${result.matches.length} findings | ${blocked} blocked, ${redacted} redacted
|
|
19067
|
-
`));
|
|
19068
|
-
process.exit(1);
|
|
19069
|
-
});
|
|
19070
|
-
cmd.addCommand(ciSubcommand());
|
|
19071
|
-
cmd.addCommand(commitRangeSubcommand());
|
|
19072
|
-
cmd.addCommand(preCommitSubcommand());
|
|
19073
|
-
cmd.addCommand(prePushSubcommand());
|
|
19074
|
-
cmd.addCommand(pathSubcommand());
|
|
19075
|
-
cmd.exitOverride((err) => {
|
|
19076
|
-
process.exit(err.exitCode === 0 ? 0 : 3);
|
|
19077
|
-
});
|
|
19078
|
-
return cmd;
|
|
19079
|
-
}
|
|
19080
|
-
|
|
19081
|
-
// src/commands/status.ts
|
|
19082
|
-
init_esm_shims();
|
|
19083
|
-
import { Command as Command5 } from "commander";
|
|
19084
|
-
import chalk5 from "chalk";
|
|
19085
|
-
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
19086
|
-
import { join as join8 } from "path";
|
|
19087
|
-
function statusCommand() {
|
|
19088
|
-
return new Command5("status").description("Show Pretense protection status").action(async () => {
|
|
19089
|
-
const dir = process.cwd();
|
|
19090
|
-
const fingerprintPath = join8(dir, ".pretense", "fingerprint.json");
|
|
19091
|
-
const configPath = join8(dir, "pretense.yaml");
|
|
19092
|
-
console.log(chalk5.cyan("\n Pretense Status\n"));
|
|
19093
|
-
const record = readProxyRecord(pidFilePath());
|
|
19094
|
-
if (record && !isProcessAlive(record.pid)) {
|
|
19095
|
-
console.log(
|
|
19096
|
-
chalk5.yellow(
|
|
19097
|
-
` \u25CB Proxy not running ${chalk5.dim(`(stale record for pid ${record.pid}; run: pretense start)`)}`
|
|
19098
|
-
)
|
|
19099
|
-
);
|
|
19100
|
-
} else {
|
|
19101
|
-
const port = record && record.port > 0 ? record.port : 9339;
|
|
19102
|
-
try {
|
|
19103
|
-
const resp = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(1e3) });
|
|
19104
|
-
if (!resp.ok) throw new Error(`health returned ${resp.status}`);
|
|
19105
|
-
const health = await resp.json();
|
|
19106
|
-
console.log(chalk5.green(` \u2713 Proxy running on :${port} (uptime: ${health["uptime"]}s)`));
|
|
19107
|
-
const stats = health["stats"];
|
|
19108
|
-
console.log(chalk5.dim(` Requests: ${stats["requestsProcessed"] ?? 0} | Mutations: ${stats["mutationsApplied"] ?? 0} | Blocked: ${stats["secretsBlocked"] ?? 0}`));
|
|
19109
|
-
} catch {
|
|
19110
|
-
console.log(chalk5.yellow(` \u25CB Proxy not running ${chalk5.dim("(run: pretense start)")}`));
|
|
19111
|
-
}
|
|
19112
|
-
}
|
|
19113
|
-
if (existsSync8(configPath)) {
|
|
19114
|
-
console.log(chalk5.green(` \u2713 Config: pretense.yaml`));
|
|
19115
|
-
} else {
|
|
19116
|
-
console.log(chalk5.yellow(` \u25CB No config ${chalk5.dim("(run: pretense config)")}`));
|
|
19117
|
-
}
|
|
19118
|
-
if (existsSync8(fingerprintPath)) {
|
|
19119
|
-
try {
|
|
19120
|
-
const fp = JSON.parse(readFileSync8(fingerprintPath, "utf-8"));
|
|
19121
|
-
const age = Math.round((Date.now() - fp.scannedAt) / (60 * 1e3));
|
|
19122
|
-
console.log(chalk5.green(` \u2713 Fingerprint: ${fp.filesScanned} files, ${fp.functions.length + fp.classes.length + fp.constants.length} identifiers`));
|
|
19123
|
-
console.log(chalk5.dim(` Last scanned: ${age} minutes ago`));
|
|
19124
|
-
} catch {
|
|
19125
|
-
console.log(chalk5.yellow(" \u26A0 Fingerprint corrupted (run: pretense init)"));
|
|
19126
|
-
}
|
|
19127
|
-
} else {
|
|
19128
|
-
console.log(chalk5.yellow(` \u25CB No fingerprint ${chalk5.dim("(run: pretense init)")}`));
|
|
19129
|
-
}
|
|
19130
|
-
console.log(chalk5.dim(`
|
|
19131
|
-
Scanner: ${getPatternCount()} patterns active
|
|
19132
|
-
`));
|
|
19133
|
-
});
|
|
19134
|
-
}
|
|
19135
|
-
|
|
19136
|
-
// src/commands/config.ts
|
|
19137
|
-
init_esm_shims();
|
|
19138
|
-
import { Command as Command6 } from "commander";
|
|
19139
|
-
import chalk6 from "chalk";
|
|
19140
|
-
import { writeFileSync as writeFileSync4, readFileSync as readFileSync9, existsSync as existsSync9 } from "fs";
|
|
19141
|
-
import { join as join9 } from "path";
|
|
19142
|
-
import YAML2 from "yaml";
|
|
19143
|
-
var DEFAULT_CONFIG = {
|
|
19144
|
-
version: 1,
|
|
19145
|
-
languages: ["typescript", "python", "java", "go", "ruby", "rust"],
|
|
19146
|
-
exclude_paths: ["node_modules", ".git", "dist"],
|
|
19147
|
-
mutation_mode: "full",
|
|
19148
|
-
output_format: "interactive"
|
|
19149
|
-
};
|
|
19150
|
-
function findConfigPath() {
|
|
19151
|
-
const cwd = process.cwd();
|
|
19152
|
-
const candidates = [".pretense.yaml", "pretense.yaml"];
|
|
19153
|
-
for (const name of candidates) {
|
|
19154
|
-
const full = join9(cwd, name);
|
|
19155
|
-
if (existsSync9(full)) return full;
|
|
19156
|
-
}
|
|
19157
|
-
return null;
|
|
19158
|
-
}
|
|
19159
|
-
function loadConfig2() {
|
|
19160
|
-
const path2 = findConfigPath();
|
|
19161
|
-
if (!path2) return { ...DEFAULT_CONFIG };
|
|
19162
|
-
try {
|
|
19163
|
-
const raw2 = readFileSync9(path2, "utf-8");
|
|
19164
|
-
return YAML2.parse(raw2) ?? { ...DEFAULT_CONFIG };
|
|
19165
|
-
} catch {
|
|
19166
|
-
return { ...DEFAULT_CONFIG };
|
|
19167
|
-
}
|
|
19168
|
-
}
|
|
19169
|
-
function configFilePath() {
|
|
19170
|
-
return findConfigPath() ?? join9(process.cwd(), "pretense.yaml");
|
|
19171
|
-
}
|
|
19172
|
-
function getNestedValue(obj, key) {
|
|
19173
|
-
const parts = key.split(".");
|
|
19174
|
-
let cur = obj;
|
|
19175
|
-
for (const p of parts) {
|
|
19176
|
-
if (cur == null || typeof cur !== "object") return void 0;
|
|
19177
|
-
cur = cur[p];
|
|
19178
|
-
}
|
|
19179
|
-
return cur;
|
|
19180
|
-
}
|
|
19181
|
-
function setNestedValue(obj, key, value) {
|
|
19182
|
-
const parts = key.split(".");
|
|
19183
|
-
const leaf = parts.pop();
|
|
19184
|
-
if (leaf === void 0) return;
|
|
19185
|
-
let cur = obj;
|
|
19186
|
-
for (const p of parts) {
|
|
19187
|
-
const child = cur[p];
|
|
19188
|
-
if (child == null || typeof child !== "object" || Array.isArray(child)) {
|
|
19189
|
-
cur[p] = {};
|
|
19190
|
-
}
|
|
19191
|
-
cur = cur[p];
|
|
19192
|
-
}
|
|
19193
|
-
cur[leaf] = value;
|
|
19194
|
-
}
|
|
19195
|
-
function parseValue(raw2) {
|
|
19196
|
-
if (raw2.startsWith("[") && raw2.endsWith("]")) {
|
|
19197
|
-
return raw2.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
19198
|
-
}
|
|
19199
|
-
if (raw2 === "true") return true;
|
|
19200
|
-
if (raw2 === "false") return false;
|
|
19201
|
-
const n = Number(raw2);
|
|
19202
|
-
if (!Number.isNaN(n) && raw2.length > 0) return n;
|
|
19203
|
-
return raw2;
|
|
19204
|
-
}
|
|
19205
|
-
function configCommand() {
|
|
19206
|
-
const cmd = new Command6("config").description("Manage pretense configuration");
|
|
19207
|
-
cmd.command("list").description("Print current configuration").action(() => {
|
|
19208
|
-
const config = loadConfig2();
|
|
19209
|
-
const path2 = findConfigPath();
|
|
19210
|
-
console.log(
|
|
19211
|
-
chalk6.cyan(`
|
|
19212
|
-
Pretense Config`) + chalk6.dim(` (${path2 ?? "defaults - no config file found"})
|
|
19213
|
-
`)
|
|
19214
|
-
);
|
|
19215
|
-
console.log(YAML2.stringify(config).replace(/^/gm, " "));
|
|
19216
|
-
});
|
|
19217
|
-
cmd.command("get").description("Print a single config value").argument("<key>", "Config key (supports dotted paths)").action((key) => {
|
|
19218
|
-
const config = loadConfig2();
|
|
19219
|
-
const val = getNestedValue(config, key);
|
|
19220
|
-
if (val === void 0) {
|
|
19221
|
-
console.error(chalk6.red(`
|
|
19222
|
-
\u2717 Key not found: ${key}
|
|
19223
|
-
`));
|
|
19224
|
-
process.exit(1);
|
|
19513
|
+
chalk5.yellow("\n x Nothing to scan \u2014 no file argument and empty input.\n") + chalk5.dim(" Scan this project: ") + chalk5.cyan("pretense scan .") + "\n" + chalk5.dim(" Scan a path: ") + chalk5.cyan("pretense scan <file-or-dir>") + "\n" + chalk5.dim(" Or pipe content: ") + chalk5.cyan("cat file | pretense scan") + "\n"
|
|
19514
|
+
);
|
|
19515
|
+
process.exit(USAGE_EXIT);
|
|
19516
|
+
}
|
|
19225
19517
|
}
|
|
19226
|
-
const
|
|
19227
|
-
|
|
19228
|
-
|
|
19229
|
-
|
|
19230
|
-
|
|
19231
|
-
|
|
19232
|
-
|
|
19233
|
-
|
|
19234
|
-
|
|
19235
|
-
|
|
19236
|
-
|
|
19518
|
+
const rawResult = await scanContent(content, opts, file);
|
|
19519
|
+
const filteredMatches = filterBySeverity(rawResult.matches, opts?.severity);
|
|
19520
|
+
const result = { ...rawResult, matches: filteredMatches, clean: filteredMatches.length === 0 };
|
|
19521
|
+
const fmt2 = resolveFormat(opts ?? {});
|
|
19522
|
+
if (fmt2 === "json") {
|
|
19523
|
+
console.log(JSON.stringify(emitResult(result, opts ?? {}), null, 2));
|
|
19524
|
+
process.exit(result.clean ? 0 : 1);
|
|
19525
|
+
}
|
|
19526
|
+
if (fmt2 === "sarif") {
|
|
19527
|
+
const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
|
|
19528
|
+
console.log(JSON.stringify(toSarif(fileResults, opts ?? {}), null, 2));
|
|
19529
|
+
process.exit(result.clean ? 0 : 1);
|
|
19530
|
+
}
|
|
19531
|
+
if (fmt2 === "csv") {
|
|
19532
|
+
const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
|
|
19533
|
+
console.log(toCsv(fileResults, opts ?? {}));
|
|
19534
|
+
process.exit(result.clean ? 0 : 1);
|
|
19535
|
+
}
|
|
19536
|
+
console.log(chalk5.cyan(`
|
|
19537
|
+
Pretense Scan \u2014 ${file ?? "stdin"}
|
|
19237
19538
|
`));
|
|
19238
|
-
})
|
|
19239
|
-
cmd.option("--force", "Overwrite existing config with defaults", false).action((opts) => {
|
|
19240
|
-
if (opts.force) {
|
|
19241
|
-
const outPath = join9(process.cwd(), "pretense.yaml");
|
|
19242
|
-
writeFileSync4(outPath, YAML2.stringify(DEFAULT_CONFIG));
|
|
19243
|
-
console.log(chalk6.green(`
|
|
19244
|
-
\u2713 Created pretense.yaml
|
|
19539
|
+
console.log(chalk5.dim(` Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)} | Duration: ${result.durationMs}ms
|
|
19245
19540
|
`));
|
|
19246
|
-
|
|
19247
|
-
|
|
19541
|
+
{
|
|
19542
|
+
const policyLine = policyNotice(opts);
|
|
19543
|
+
if (policyLine) console.log(policyLine + "\n");
|
|
19544
|
+
}
|
|
19545
|
+
if (opts.level !== 1 && result.l2 && !result.l2.ran && result.l2.reason) {
|
|
19546
|
+
console.log(chalk5.yellow(` ! ${result.l2.reason}`));
|
|
19547
|
+
console.log(chalk5.dim(" L1 (regex) ran in full; no finding was dropped.\n"));
|
|
19548
|
+
}
|
|
19549
|
+
if (result.clean) {
|
|
19550
|
+
console.log(chalk5.green(" \u2713 No secrets or PII detected\n"));
|
|
19551
|
+
process.exit(0);
|
|
19552
|
+
}
|
|
19553
|
+
for (const match3 of result.matches) {
|
|
19554
|
+
const actionColor = match3.action === "block" ? chalk5.red : match3.action === "redact" ? chalk5.yellow : chalk5.dim;
|
|
19555
|
+
console.log(
|
|
19556
|
+
` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk5.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk5.dim(rowTail(match3, opts ?? {}))}`
|
|
19557
|
+
);
|
|
19248
19558
|
}
|
|
19559
|
+
const blocked = result.matches.filter((m) => m.action === "block").length;
|
|
19560
|
+
const redacted = result.matches.filter((m) => m.action === "redact").length;
|
|
19561
|
+
console.log(chalk5.dim(`
|
|
19562
|
+
Total: ${result.matches.length} findings | ${blocked} blocked, ${redacted} redacted
|
|
19563
|
+
`));
|
|
19564
|
+
process.exit(1);
|
|
19565
|
+
});
|
|
19566
|
+
cmd.addCommand(ciSubcommand());
|
|
19567
|
+
cmd.addCommand(commitRangeSubcommand());
|
|
19568
|
+
cmd.addCommand(preCommitSubcommand());
|
|
19569
|
+
cmd.addCommand(prePushSubcommand());
|
|
19570
|
+
cmd.addCommand(pathSubcommand());
|
|
19571
|
+
cmd.exitOverride((err) => {
|
|
19572
|
+
process.exit(err.exitCode === 0 ? 0 : 3);
|
|
19249
19573
|
});
|
|
19250
19574
|
return cmd;
|
|
19251
19575
|
}
|
|
19252
19576
|
|
|
19253
|
-
// src/commands/
|
|
19577
|
+
// src/commands/status.ts
|
|
19254
19578
|
init_esm_shims();
|
|
19255
|
-
import { Command as
|
|
19256
|
-
import
|
|
19257
|
-
import {
|
|
19258
|
-
import { join as
|
|
19259
|
-
function
|
|
19260
|
-
return new
|
|
19261
|
-
const
|
|
19262
|
-
|
|
19263
|
-
|
|
19264
|
-
|
|
19265
|
-
|
|
19266
|
-
|
|
19267
|
-
|
|
19268
|
-
|
|
19269
|
-
|
|
19270
|
-
|
|
19271
|
-
|
|
19579
|
+
import { Command as Command6 } from "commander";
|
|
19580
|
+
import chalk6 from "chalk";
|
|
19581
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
19582
|
+
import { join as join9 } from "path";
|
|
19583
|
+
function statusCommand() {
|
|
19584
|
+
return new Command6("status").description("Show Pretense protection status").action(async () => {
|
|
19585
|
+
const dir = process.cwd();
|
|
19586
|
+
const fingerprintPath = join9(dir, ".pretense", "fingerprint.json");
|
|
19587
|
+
const configPath = join9(dir, "pretense.yaml");
|
|
19588
|
+
console.log(chalk6.cyan("\n Pretense Status\n"));
|
|
19589
|
+
const record = readProxyRecord(pidFilePath());
|
|
19590
|
+
if (record && !isProcessAlive(record.pid)) {
|
|
19591
|
+
console.log(
|
|
19592
|
+
chalk6.yellow(
|
|
19593
|
+
` \u25CB Proxy not running ${chalk6.dim(`(stale record for pid ${record.pid}; run: pretense start)`)}`
|
|
19594
|
+
)
|
|
19595
|
+
);
|
|
19596
|
+
} else {
|
|
19597
|
+
const port = record && record.port > 0 ? record.port : 9339;
|
|
19598
|
+
try {
|
|
19599
|
+
const resp = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(1e3) });
|
|
19600
|
+
if (!resp.ok) throw new Error(`health returned ${resp.status}`);
|
|
19601
|
+
const health = await resp.json();
|
|
19602
|
+
console.log(chalk6.green(` \u2713 Proxy running on :${port} (uptime: ${health["uptime"]}s)`));
|
|
19603
|
+
const stats = health["stats"];
|
|
19604
|
+
console.log(chalk6.dim(` Requests: ${stats["requestsProcessed"] ?? 0} | Mutations: ${stats["mutationsApplied"] ?? 0} | Blocked: ${stats["secretsBlocked"] ?? 0}`));
|
|
19605
|
+
} catch {
|
|
19606
|
+
console.log(chalk6.yellow(` \u25CB Proxy not running ${chalk6.dim("(run: pretense start)")}`));
|
|
19272
19607
|
}
|
|
19273
|
-
console.error(chalk7.dim("\n No audit data available. The audit database has not been initialized yet."));
|
|
19274
|
-
console.error(chalk7.dim(" Run " + chalk7.cyan("pretense start") + " and make some requests first.\n"));
|
|
19275
|
-
return;
|
|
19276
19608
|
}
|
|
19277
|
-
if (
|
|
19278
|
-
console.log(
|
|
19279
|
-
|
|
19609
|
+
if (existsSync9(configPath)) {
|
|
19610
|
+
console.log(chalk6.green(` \u2713 Config: pretense.yaml`));
|
|
19611
|
+
} else {
|
|
19612
|
+
console.log(chalk6.yellow(` \u25CB No config ${chalk6.dim("(run: pretense config)")}`));
|
|
19280
19613
|
}
|
|
19281
|
-
if (
|
|
19282
|
-
|
|
19283
|
-
|
|
19614
|
+
if (existsSync9(fingerprintPath)) {
|
|
19615
|
+
try {
|
|
19616
|
+
const fp = JSON.parse(readFileSync9(fingerprintPath, "utf-8"));
|
|
19617
|
+
const age = Math.round((Date.now() - fp.scannedAt) / (60 * 1e3));
|
|
19618
|
+
console.log(chalk6.green(` \u2713 Fingerprint: ${fp.filesScanned} files, ${fp.functions.length + fp.classes.length + fp.constants.length} identifiers`));
|
|
19619
|
+
console.log(chalk6.dim(` Last scanned: ${age} minutes ago`));
|
|
19620
|
+
} catch {
|
|
19621
|
+
console.log(chalk6.yellow(" \u26A0 Fingerprint corrupted (run: pretense init)"));
|
|
19622
|
+
}
|
|
19623
|
+
} else {
|
|
19624
|
+
console.log(chalk6.yellow(` \u25CB No fingerprint ${chalk6.dim("(run: pretense init)")}`));
|
|
19284
19625
|
}
|
|
19285
|
-
console.log(
|
|
19286
|
-
|
|
19626
|
+
console.log(chalk6.dim(`
|
|
19627
|
+
Scanner: ${getPatternCount()} patterns active
|
|
19287
19628
|
`));
|
|
19288
|
-
console.log(chalk7.dim(" " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATIONS".padEnd(12) + "BLOCKED".padEnd(10) + "MS"));
|
|
19289
|
-
console.log(chalk7.dim(" " + "\u2500".repeat(70)));
|
|
19290
|
-
for (const entry of entries) {
|
|
19291
|
-
const time = new Date(entry.timestamp).toTimeString().slice(0, 8);
|
|
19292
|
-
const actionColor = entry.action === "blocked" ? chalk7.red : entry.action === "mutated" ? chalk7.green : entry.action === "redacted" ? chalk7.yellow : entry.action === "error" ? chalk7.red : chalk7.dim;
|
|
19293
|
-
console.log(
|
|
19294
|
-
" " + chalk7.dim(time.padEnd(10)) + actionColor(entry.action.padEnd(12)) + chalk7.dim(entry.provider.padEnd(14)) + chalk7.dim(String(entry.mutationsApplied).padEnd(12)) + (entry.secretsBlocked > 0 ? chalk7.red(String(entry.secretsBlocked).padEnd(10)) : chalk7.dim("0".padEnd(10))) + chalk7.dim(String(entry.scanDurationMs).slice(0, 5))
|
|
19295
|
-
);
|
|
19296
|
-
}
|
|
19297
|
-
console.log();
|
|
19298
19629
|
});
|
|
19299
19630
|
}
|
|
19300
19631
|
|
|
19301
|
-
// src/commands/
|
|
19302
|
-
init_esm_shims();
|
|
19303
|
-
import { Command as Command8 } from "commander";
|
|
19304
|
-
import chalk8 from "chalk";
|
|
19305
|
-
import { join as join12 } from "path";
|
|
19306
|
-
import { homedir as homedir6 } from "os";
|
|
19307
|
-
|
|
19308
|
-
// src/plan-usage.ts
|
|
19632
|
+
// src/commands/config.ts
|
|
19309
19633
|
init_esm_shims();
|
|
19310
|
-
import {
|
|
19311
|
-
import
|
|
19312
|
-
import {
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19320
|
-
|
|
19321
|
-
|
|
19322
|
-
|
|
19323
|
-
|
|
19324
|
-
|
|
19325
|
-
|
|
19326
|
-
|
|
19327
|
-
|
|
19328
|
-
}
|
|
19329
|
-
|
|
19330
|
-
|
|
19331
|
-
function
|
|
19332
|
-
const
|
|
19333
|
-
if (!
|
|
19334
|
-
const normalized = /^https?:\/\//i.test(raw2) ? raw2 : `https://${raw2}`;
|
|
19335
|
-
try {
|
|
19336
|
-
const u = new URL(normalized);
|
|
19337
|
-
const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL : u.origin;
|
|
19338
|
-
const path2 = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
19339
|
-
return `${origin}${path2}`;
|
|
19340
|
-
} catch {
|
|
19341
|
-
return DEFAULT_PRETENSE_API_URL;
|
|
19342
|
-
}
|
|
19343
|
-
}
|
|
19344
|
-
function tierRequestTimeoutMs() {
|
|
19345
|
-
const raw2 = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
|
|
19346
|
-
if (!raw2 || !/^\d+$/.test(raw2)) return DEFAULT_TIER_TIMEOUT_MS;
|
|
19347
|
-
return Math.min(Math.max(parseInt(raw2, 10), 1e3), 12e4);
|
|
19348
|
-
}
|
|
19349
|
-
function planToTier(plan) {
|
|
19350
|
-
switch (String(plan).toUpperCase()) {
|
|
19351
|
-
case "FREE":
|
|
19352
|
-
return "free";
|
|
19353
|
-
case "PRO":
|
|
19354
|
-
return "pro";
|
|
19355
|
-
case "ENTERPRISE":
|
|
19356
|
-
return "enterprise";
|
|
19357
|
-
default:
|
|
19358
|
-
return null;
|
|
19359
|
-
}
|
|
19360
|
-
}
|
|
19361
|
-
async function tierFromBackend(apiKey) {
|
|
19362
|
-
const url = `${getConfiguredApiRoot()}/api/cli/auth/validate`;
|
|
19363
|
-
const controller = new AbortController();
|
|
19364
|
-
const timer = setTimeout(() => controller.abort(), tierRequestTimeoutMs());
|
|
19365
|
-
try {
|
|
19366
|
-
const resp = await fetch(url, {
|
|
19367
|
-
method: "POST",
|
|
19368
|
-
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
19369
|
-
body: "{}",
|
|
19370
|
-
signal: controller.signal
|
|
19371
|
-
});
|
|
19372
|
-
if (resp.status === 401 || resp.status === 403) {
|
|
19373
|
-
return {
|
|
19374
|
-
tier: null,
|
|
19375
|
-
source: "unverified",
|
|
19376
|
-
detail: "the server rejected this API key \u2014 run `pretense auth login` again"
|
|
19377
|
-
};
|
|
19378
|
-
}
|
|
19379
|
-
if (!resp.ok) {
|
|
19380
|
-
return {
|
|
19381
|
-
tier: null,
|
|
19382
|
-
source: "unverified",
|
|
19383
|
-
detail: `the plan service returned HTTP ${resp.status}`
|
|
19384
|
-
};
|
|
19385
|
-
}
|
|
19386
|
-
const body = await resp.json();
|
|
19387
|
-
const tier = planToTier(body["plan"]);
|
|
19388
|
-
if (body["valid"] === false || !tier) {
|
|
19389
|
-
return {
|
|
19390
|
-
tier: null,
|
|
19391
|
-
source: "unverified",
|
|
19392
|
-
detail: "the plan service did not report a recognizable plan"
|
|
19393
|
-
};
|
|
19394
|
-
}
|
|
19395
|
-
return { tier, source: "backend" };
|
|
19396
|
-
} catch (err) {
|
|
19397
|
-
const aborted = err?.name === "AbortError";
|
|
19398
|
-
return {
|
|
19399
|
-
tier: null,
|
|
19400
|
-
source: "unverified",
|
|
19401
|
-
detail: aborted ? `could not reach ${url} within ${tierRequestTimeoutMs()}ms` : `could not reach ${url}`
|
|
19402
|
-
};
|
|
19403
|
-
} finally {
|
|
19404
|
-
clearTimeout(timer);
|
|
19405
|
-
}
|
|
19406
|
-
}
|
|
19407
|
-
async function resolvePlanTier(opts = {}) {
|
|
19408
|
-
const apiKey = readApiKey();
|
|
19409
|
-
if (!apiKey) return { tier: "free", source: "no-credentials" };
|
|
19410
|
-
const fromPrefix = tierFromApiKey(apiKey);
|
|
19411
|
-
if (fromPrefix) return { tier: fromPrefix, source: "key-prefix" };
|
|
19412
|
-
if (opts.offline) {
|
|
19413
|
-
return {
|
|
19414
|
-
tier: null,
|
|
19415
|
-
source: "unverified",
|
|
19416
|
-
detail: "--offline was set, and a dashboard key carries no plan of its own"
|
|
19417
|
-
};
|
|
19418
|
-
}
|
|
19419
|
-
return tierFromBackend(apiKey);
|
|
19420
|
-
}
|
|
19421
|
-
var QUOTA_PERIOD_DAYS2 = 7;
|
|
19422
|
-
function collectUsage(dbPath) {
|
|
19423
|
-
const now = /* @__PURE__ */ new Date();
|
|
19424
|
-
const periodMs = QUOTA_PERIOD_DAYS2 * 24 * 60 * 60 * 1e3;
|
|
19425
|
-
const periodStartTs = Date.now() - periodMs;
|
|
19426
|
-
const sinceMs = periodMs;
|
|
19427
|
-
let store = null;
|
|
19634
|
+
import { Command as Command7 } from "commander";
|
|
19635
|
+
import chalk7 from "chalk";
|
|
19636
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
|
|
19637
|
+
import { join as join10 } from "path";
|
|
19638
|
+
import YAML2 from "yaml";
|
|
19639
|
+
var DEFAULT_CONFIG = {
|
|
19640
|
+
version: 1,
|
|
19641
|
+
languages: ["typescript", "python", "java", "go", "ruby", "rust"],
|
|
19642
|
+
exclude_paths: ["node_modules", ".git", "dist"],
|
|
19643
|
+
mutation_mode: "full",
|
|
19644
|
+
output_format: "interactive"
|
|
19645
|
+
};
|
|
19646
|
+
function findConfigPath() {
|
|
19647
|
+
const cwd = process.cwd();
|
|
19648
|
+
const candidates = [".pretense.yaml", "pretense.yaml"];
|
|
19649
|
+
for (const name of candidates) {
|
|
19650
|
+
const full = join10(cwd, name);
|
|
19651
|
+
if (existsSync10(full)) return full;
|
|
19652
|
+
}
|
|
19653
|
+
return null;
|
|
19654
|
+
}
|
|
19655
|
+
function loadConfig2() {
|
|
19656
|
+
const path2 = findConfigPath();
|
|
19657
|
+
if (!path2) return { ...DEFAULT_CONFIG };
|
|
19428
19658
|
try {
|
|
19429
|
-
|
|
19430
|
-
|
|
19431
|
-
return {
|
|
19432
|
-
periodStartTs,
|
|
19433
|
-
metrics: {
|
|
19434
|
-
tenantId: "local",
|
|
19435
|
-
// The trailing window these numbers actually cover — previously the
|
|
19436
|
-
// calendar month, which did not match the aggregation OR the quota.
|
|
19437
|
-
periodStart: new Date(periodStartTs).toISOString().split("T")[0],
|
|
19438
|
-
periodEnd: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
19439
|
-
mutationsApplied: stats.totalMutationsApplied,
|
|
19440
|
-
secretsBlocked: stats.totalSecretsBlocked,
|
|
19441
|
-
piiRedacted: stats.totalPiiRedacted,
|
|
19442
|
-
totalScans: stats.totalRequests,
|
|
19443
|
-
bytesProtected: 0,
|
|
19444
|
-
activeSeats: 1,
|
|
19445
|
-
providers: Object.fromEntries(stats.topProviders.map((p) => [p.provider, p.count]))
|
|
19446
|
-
}
|
|
19447
|
-
};
|
|
19659
|
+
const raw2 = readFileSync10(path2, "utf-8");
|
|
19660
|
+
return YAML2.parse(raw2) ?? { ...DEFAULT_CONFIG };
|
|
19448
19661
|
} catch {
|
|
19449
|
-
return
|
|
19450
|
-
} finally {
|
|
19451
|
-
store?.close();
|
|
19662
|
+
return { ...DEFAULT_CONFIG };
|
|
19452
19663
|
}
|
|
19453
19664
|
}
|
|
19454
|
-
function
|
|
19455
|
-
return
|
|
19665
|
+
function configFilePath() {
|
|
19666
|
+
return findConfigPath() ?? join10(process.cwd(), "pretense.yaml");
|
|
19456
19667
|
}
|
|
19457
|
-
function
|
|
19458
|
-
const
|
|
19459
|
-
|
|
19460
|
-
|
|
19668
|
+
function getNestedValue(obj, key) {
|
|
19669
|
+
const parts = key.split(".");
|
|
19670
|
+
let cur = obj;
|
|
19671
|
+
for (const p of parts) {
|
|
19672
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
19673
|
+
cur = cur[p];
|
|
19674
|
+
}
|
|
19675
|
+
return cur;
|
|
19461
19676
|
}
|
|
19462
|
-
function
|
|
19463
|
-
|
|
19677
|
+
function setNestedValue(obj, key, value) {
|
|
19678
|
+
const parts = key.split(".");
|
|
19679
|
+
const leaf = parts.pop();
|
|
19680
|
+
if (leaf === void 0) return;
|
|
19681
|
+
let cur = obj;
|
|
19682
|
+
for (const p of parts) {
|
|
19683
|
+
const child = cur[p];
|
|
19684
|
+
if (child == null || typeof child !== "object" || Array.isArray(child)) {
|
|
19685
|
+
cur[p] = {};
|
|
19686
|
+
}
|
|
19687
|
+
cur = cur[p];
|
|
19688
|
+
}
|
|
19689
|
+
cur[leaf] = value;
|
|
19464
19690
|
}
|
|
19465
|
-
function
|
|
19466
|
-
if (
|
|
19467
|
-
|
|
19468
|
-
|
|
19691
|
+
function parseValue(raw2) {
|
|
19692
|
+
if (raw2.startsWith("[") && raw2.endsWith("]")) {
|
|
19693
|
+
return raw2.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
19694
|
+
}
|
|
19695
|
+
if (raw2 === "true") return true;
|
|
19696
|
+
if (raw2 === "false") return false;
|
|
19697
|
+
const n = Number(raw2);
|
|
19698
|
+
if (!Number.isNaN(n) && raw2.length > 0) return n;
|
|
19699
|
+
return raw2;
|
|
19469
19700
|
}
|
|
19470
|
-
function
|
|
19471
|
-
|
|
19472
|
-
|
|
19473
|
-
|
|
19474
|
-
|
|
19701
|
+
function configCommand() {
|
|
19702
|
+
const cmd = new Command7("config").description("Manage pretense configuration");
|
|
19703
|
+
cmd.command("list").description("Print current configuration").action(() => {
|
|
19704
|
+
const config = loadConfig2();
|
|
19705
|
+
const path2 = findConfigPath();
|
|
19706
|
+
console.log(
|
|
19707
|
+
chalk7.cyan(`
|
|
19708
|
+
Pretense Config`) + chalk7.dim(` (${path2 ?? "defaults - no config file found"})
|
|
19709
|
+
`)
|
|
19710
|
+
);
|
|
19711
|
+
console.log(YAML2.stringify(config).replace(/^/gm, " "));
|
|
19712
|
+
});
|
|
19713
|
+
cmd.command("get").description("Print a single config value").argument("<key>", "Config key (supports dotted paths)").action((key) => {
|
|
19714
|
+
const config = loadConfig2();
|
|
19715
|
+
const val = getNestedValue(config, key);
|
|
19716
|
+
if (val === void 0) {
|
|
19717
|
+
console.error(chalk7.red(`
|
|
19718
|
+
\u2717 Key not found: ${key}
|
|
19719
|
+
`));
|
|
19720
|
+
process.exit(1);
|
|
19721
|
+
}
|
|
19722
|
+
const display = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
19723
|
+
console.log(display);
|
|
19724
|
+
});
|
|
19725
|
+
cmd.command("set").description("Set a config value").argument("<key>", "Config key (supports dotted paths)").argument("<value>", "Value to set").action((key, rawValue) => {
|
|
19726
|
+
const config = loadConfig2();
|
|
19727
|
+
const value = parseValue(rawValue);
|
|
19728
|
+
setNestedValue(config, key, value);
|
|
19729
|
+
const outPath = configFilePath();
|
|
19730
|
+
writeFileSync4(outPath, YAML2.stringify(config));
|
|
19731
|
+
console.log(chalk7.green(`
|
|
19732
|
+
\u2713 Set ${key} = ${JSON.stringify(value)} in ${outPath}
|
|
19733
|
+
`));
|
|
19734
|
+
});
|
|
19735
|
+
cmd.option("--force", "Overwrite existing config with defaults", false).action((opts) => {
|
|
19736
|
+
if (opts.force) {
|
|
19737
|
+
const outPath = join10(process.cwd(), "pretense.yaml");
|
|
19738
|
+
writeFileSync4(outPath, YAML2.stringify(DEFAULT_CONFIG));
|
|
19739
|
+
console.log(chalk7.green(`
|
|
19740
|
+
\u2713 Created pretense.yaml
|
|
19741
|
+
`));
|
|
19742
|
+
} else {
|
|
19743
|
+
cmd.help();
|
|
19744
|
+
}
|
|
19745
|
+
});
|
|
19746
|
+
return cmd;
|
|
19747
|
+
}
|
|
19748
|
+
|
|
19749
|
+
// src/commands/logs.ts
|
|
19750
|
+
init_esm_shims();
|
|
19751
|
+
import { Command as Command8 } from "commander";
|
|
19752
|
+
import chalk8 from "chalk";
|
|
19753
|
+
import { homedir as homedir5 } from "os";
|
|
19754
|
+
import { join as join11 } from "path";
|
|
19755
|
+
function logsCommand() {
|
|
19756
|
+
return new Command8("logs").description("Show recent audit log entries").option("-l, --limit <n>", "Number of entries to show", "50").option("--json", "Output as JSON", false).action((opts) => {
|
|
19757
|
+
const limit = parseInt(opts.limit, 10);
|
|
19758
|
+
let store;
|
|
19759
|
+
let entries;
|
|
19760
|
+
try {
|
|
19761
|
+
store = new AuditStore(join11(homedir5(), ".pretense", "audit.db"));
|
|
19762
|
+
entries = store.getRecentEntries(limit);
|
|
19763
|
+
store.close();
|
|
19764
|
+
} catch {
|
|
19765
|
+
if (opts.json) {
|
|
19766
|
+
console.log("[]");
|
|
19767
|
+
return;
|
|
19768
|
+
}
|
|
19769
|
+
console.error(chalk8.dim("\n No audit data available. The audit database has not been initialized yet."));
|
|
19770
|
+
console.error(chalk8.dim(" Run " + chalk8.cyan("pretense start") + " and make some requests first.\n"));
|
|
19771
|
+
return;
|
|
19772
|
+
}
|
|
19773
|
+
if (opts.json) {
|
|
19774
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
19775
|
+
return;
|
|
19776
|
+
}
|
|
19777
|
+
if (entries.length === 0) {
|
|
19778
|
+
console.log(chalk8.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
|
|
19779
|
+
return;
|
|
19780
|
+
}
|
|
19781
|
+
console.log(chalk8.cyan(`
|
|
19782
|
+
Pretense Audit Log \u2014 last ${entries.length} entries
|
|
19783
|
+
`));
|
|
19784
|
+
console.log(chalk8.dim(" " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATIONS".padEnd(12) + "BLOCKED".padEnd(10) + "MS"));
|
|
19785
|
+
console.log(chalk8.dim(" " + "\u2500".repeat(70)));
|
|
19786
|
+
for (const entry of entries) {
|
|
19787
|
+
const time = new Date(entry.timestamp).toTimeString().slice(0, 8);
|
|
19788
|
+
const actionColor = entry.action === "blocked" ? chalk8.red : entry.action === "mutated" ? chalk8.green : entry.action === "redacted" ? chalk8.yellow : entry.action === "error" ? chalk8.red : chalk8.dim;
|
|
19789
|
+
console.log(
|
|
19790
|
+
" " + chalk8.dim(time.padEnd(10)) + actionColor(entry.action.padEnd(12)) + chalk8.dim(entry.provider.padEnd(14)) + chalk8.dim(String(entry.mutationsApplied).padEnd(12)) + (entry.secretsBlocked > 0 ? chalk8.red(String(entry.secretsBlocked).padEnd(10)) : chalk8.dim("0".padEnd(10))) + chalk8.dim(String(entry.scanDurationMs).slice(0, 5))
|
|
19791
|
+
);
|
|
19792
|
+
}
|
|
19793
|
+
console.log();
|
|
19794
|
+
});
|
|
19475
19795
|
}
|
|
19476
19796
|
|
|
19477
19797
|
// src/commands/usage.ts
|
|
19798
|
+
init_esm_shims();
|
|
19799
|
+
import { Command as Command9 } from "commander";
|
|
19800
|
+
import chalk9 from "chalk";
|
|
19801
|
+
import { join as join12 } from "path";
|
|
19802
|
+
import { homedir as homedir6 } from "os";
|
|
19478
19803
|
function renderBar(percent, width = 30) {
|
|
19479
19804
|
const filled = Math.min(Math.round(percent / 100 * width), width);
|
|
19480
19805
|
const empty = width - filled;
|
|
19481
19806
|
const bar2 = "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
19482
|
-
if (percent >= 100) return
|
|
19483
|
-
if (percent >= 90) return
|
|
19484
|
-
if (percent >= 80) return
|
|
19485
|
-
return
|
|
19807
|
+
if (percent >= 100) return chalk9.red(bar2);
|
|
19808
|
+
if (percent >= 90) return chalk9.yellow(bar2);
|
|
19809
|
+
if (percent >= 80) return chalk9.yellow(bar2);
|
|
19810
|
+
return chalk9.green(bar2);
|
|
19486
19811
|
}
|
|
19487
19812
|
function formatMetric(value, limit) {
|
|
19488
19813
|
if (limit === null) return `${value.toLocaleString("en-US")} / unlimited`;
|
|
19489
19814
|
return `${value.toLocaleString("en-US")} / ${limit.toLocaleString("en-US")}`;
|
|
19490
19815
|
}
|
|
19491
19816
|
function percentLabel(percent) {
|
|
19492
|
-
if (percent >= 100) return
|
|
19493
|
-
if (percent >= 80) return
|
|
19494
|
-
return
|
|
19817
|
+
if (percent >= 100) return chalk9.red("OVER LIMIT");
|
|
19818
|
+
if (percent >= 80) return chalk9.yellow(`${percent}%`);
|
|
19819
|
+
return chalk9.green(`${percent}%`);
|
|
19495
19820
|
}
|
|
19496
19821
|
function emptyUsagePayload(resolution) {
|
|
19497
19822
|
const tier = resolution.tier;
|
|
@@ -19513,7 +19838,7 @@ function emptyUsagePayload(resolution) {
|
|
|
19513
19838
|
};
|
|
19514
19839
|
}
|
|
19515
19840
|
function usageCommand() {
|
|
19516
|
-
return new
|
|
19841
|
+
return new Command9("usage").description("Show plan usage vs limits for the current billing period").option("--db <path>", "Path to audit database", join12(homedir6(), ".pretense", "audit.db")).option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
|
|
19517
19842
|
const resolution = await resolvePlanTier({ offline: opts.offline });
|
|
19518
19843
|
const tier = resolution.tier;
|
|
19519
19844
|
const collected = collectUsage(opts.db);
|
|
@@ -19524,7 +19849,7 @@ function usageCommand() {
|
|
|
19524
19849
|
return;
|
|
19525
19850
|
}
|
|
19526
19851
|
console.log(
|
|
19527
|
-
|
|
19852
|
+
chalk9.yellow("\n No audit data found. Run ") + chalk9.cyan("pretense start") + chalk9.yellow(" and make some requests first.\n")
|
|
19528
19853
|
);
|
|
19529
19854
|
return;
|
|
19530
19855
|
}
|
|
@@ -19567,7 +19892,7 @@ function usageCommand() {
|
|
|
19567
19892
|
}
|
|
19568
19893
|
const resetDate = resetCadenceLabel();
|
|
19569
19894
|
console.log(
|
|
19570
|
-
|
|
19895
|
+
chalk9.cyan(
|
|
19571
19896
|
"\n Pretense Usage \u2014 " + // Header shows today's date, NOT a billing month: quotas roll on a
|
|
19572
19897
|
// 7-day window anchored to signup, so a month name would imply a
|
|
19573
19898
|
// reset boundary that does not exist.
|
|
@@ -19578,11 +19903,14 @@ function usageCommand() {
|
|
|
19578
19903
|
})
|
|
19579
19904
|
)
|
|
19580
19905
|
);
|
|
19581
|
-
console.log(
|
|
19906
|
+
console.log(chalk9.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetDate}
|
|
19582
19907
|
`));
|
|
19908
|
+
for (const line of planMismatchHint(resolution)) {
|
|
19909
|
+
console.log(chalk9.dim(` ${line}`));
|
|
19910
|
+
}
|
|
19583
19911
|
if (!tier) {
|
|
19584
19912
|
for (const line of unverifiedNote(resolution.detail)) {
|
|
19585
|
-
console.log(
|
|
19913
|
+
console.log(chalk9.yellow(` ${line}`));
|
|
19586
19914
|
}
|
|
19587
19915
|
console.log();
|
|
19588
19916
|
console.log(" " + "Mutations".padEnd(20) + usageMetrics.mutationsApplied.toLocaleString("en-US"));
|
|
@@ -19595,42 +19923,42 @@ function usageCommand() {
|
|
|
19595
19923
|
const overall = maxUsagePercent(usageMetrics, tier);
|
|
19596
19924
|
const nextTier = recommendedTier(usageMetrics, tier);
|
|
19597
19925
|
console.log(
|
|
19598
|
-
" " +
|
|
19926
|
+
" " + chalk9.bold("Metric".padEnd(20)) + chalk9.bold("Used / Limit".padEnd(30)) + chalk9.bold("Progress")
|
|
19599
19927
|
);
|
|
19600
|
-
console.log(" " +
|
|
19928
|
+
console.log(" " + chalk9.dim("\u2500".repeat(72)));
|
|
19601
19929
|
const rows = [
|
|
19602
19930
|
{ label: "Mutations", value: usageMetrics.mutationsApplied, limit: planConfig.mutationsPerPeriod },
|
|
19603
19931
|
{ label: "API Requests", value: usageMetrics.totalScans, limit: planConfig.scansPerPeriod }
|
|
19604
19932
|
];
|
|
19605
19933
|
for (const row of rows) {
|
|
19606
19934
|
const percent = row.limit === null ? 0 : Math.round(row.value / row.limit * 100);
|
|
19607
|
-
const bar2 = row.limit === null ?
|
|
19935
|
+
const bar2 = row.limit === null ? chalk9.dim(" unlimited ".padEnd(32)) : renderBar(percent);
|
|
19608
19936
|
console.log(
|
|
19609
19937
|
" " + row.label.padEnd(20) + formatMetric(row.value, row.limit).padEnd(30) + bar2 + " " + (row.limit === null ? "" : percentLabel(percent))
|
|
19610
19938
|
);
|
|
19611
19939
|
}
|
|
19612
19940
|
console.log(
|
|
19613
|
-
" " + "Secrets Blocked".padEnd(20) + String(usageMetrics.secretsBlocked.toLocaleString("en-US")).padEnd(30) +
|
|
19941
|
+
" " + "Secrets Blocked".padEnd(20) + String(usageMetrics.secretsBlocked.toLocaleString("en-US")).padEnd(30) + chalk9.dim("(no limit)")
|
|
19614
19942
|
);
|
|
19615
|
-
console.log(" " +
|
|
19943
|
+
console.log(" " + chalk9.dim("\u2500".repeat(72)));
|
|
19616
19944
|
if (tier === "enterprise") {
|
|
19617
|
-
console.log(
|
|
19945
|
+
console.log(chalk9.green("\n Enterprise plan \u2014 all limits are unlimited.\n"));
|
|
19618
19946
|
} else if (overall >= 100) {
|
|
19619
|
-
console.log(
|
|
19947
|
+
console.log(chalk9.red(`
|
|
19620
19948
|
LIMIT REACHED (${overall}% of ${tierLabel(tier)} plan used).`));
|
|
19621
19949
|
} else if (overall >= 90) {
|
|
19622
19950
|
console.log(
|
|
19623
|
-
|
|
19951
|
+
chalk9.yellow(
|
|
19624
19952
|
`
|
|
19625
19953
|
WARNING: ${overall}% of your ${tierLabel(tier)} plan used \u2014 approaching limit.`
|
|
19626
19954
|
)
|
|
19627
19955
|
);
|
|
19628
19956
|
} else if (overall >= 80) {
|
|
19629
|
-
console.log(
|
|
19957
|
+
console.log(chalk9.yellow(`
|
|
19630
19958
|
NOTICE: ${overall}% of your ${tierLabel(tier)} plan used.`));
|
|
19631
19959
|
} else {
|
|
19632
19960
|
console.log(
|
|
19633
|
-
|
|
19961
|
+
chalk9.green(`
|
|
19634
19962
|
${overall}% of your ${tierLabel(tier)} plan used \u2014 you're in good shape.`)
|
|
19635
19963
|
);
|
|
19636
19964
|
}
|
|
@@ -19642,12 +19970,12 @@ function usageCommand() {
|
|
|
19642
19970
|
enterprise: "Unlimited mutations + SSO + on-prem + SIEM"
|
|
19643
19971
|
};
|
|
19644
19972
|
console.log(
|
|
19645
|
-
|
|
19973
|
+
chalk9.cyan(`
|
|
19646
19974
|
Upgrade to ${tierName(nextTier)} (${priceLabel(nextTier)}):`)
|
|
19647
19975
|
);
|
|
19648
|
-
console.log(
|
|
19976
|
+
console.log(chalk9.dim(` ${upgradeBenefit[nextTier]}`));
|
|
19649
19977
|
console.log(
|
|
19650
|
-
|
|
19978
|
+
chalk9.bold(`
|
|
19651
19979
|
\u2192 https://pretense.ai/pricing?ref=cli_usage&from=${tier}
|
|
19652
19980
|
`)
|
|
19653
19981
|
);
|
|
@@ -19659,12 +19987,12 @@ function usageCommand() {
|
|
|
19659
19987
|
|
|
19660
19988
|
// src/commands/auth.ts
|
|
19661
19989
|
init_esm_shims();
|
|
19662
|
-
import { Command as
|
|
19663
|
-
import
|
|
19990
|
+
import { Command as Command10 } from "commander";
|
|
19991
|
+
import chalk10 from "chalk";
|
|
19664
19992
|
import { existsSync as existsSync11, readFileSync as readFileSync11, unlinkSync as unlinkSync2 } from "fs";
|
|
19665
19993
|
import { join as join13 } from "path";
|
|
19666
19994
|
import { homedir as homedir7 } from "os";
|
|
19667
|
-
import { execSync as
|
|
19995
|
+
import { execSync as execSync4 } from "child_process";
|
|
19668
19996
|
var CREDENTIALS_DIR = join13(homedir7(), ".pretense");
|
|
19669
19997
|
var CREDENTIALS_PATH = join13(CREDENTIALS_DIR, "credentials.json");
|
|
19670
19998
|
async function verifyApiKey(apiKey) {
|
|
@@ -19708,7 +20036,7 @@ async function verifyApiKey(apiKey) {
|
|
|
19708
20036
|
}
|
|
19709
20037
|
}
|
|
19710
20038
|
function storageNotice() {
|
|
19711
|
-
return
|
|
20039
|
+
return chalk10.dim(` Stored: ${CREDENTIALS_PATH}`) + chalk10.dim(`
|
|
19712
20040
|
Mode: 0600 (file), 0700 (~/.pretense) \u2014 plaintext, not encrypted
|
|
19713
20041
|
`);
|
|
19714
20042
|
}
|
|
@@ -19731,15 +20059,15 @@ function maskApiKey(key) {
|
|
|
19731
20059
|
function openUrl(url) {
|
|
19732
20060
|
try {
|
|
19733
20061
|
if (process.platform === "darwin") {
|
|
19734
|
-
|
|
20062
|
+
execSync4(`open ${url}`, { stdio: "ignore" });
|
|
19735
20063
|
} else {
|
|
19736
|
-
|
|
20064
|
+
execSync4(`xdg-open ${url}`, { stdio: "ignore" });
|
|
19737
20065
|
}
|
|
19738
20066
|
} catch {
|
|
19739
20067
|
}
|
|
19740
20068
|
}
|
|
19741
20069
|
function authCommand() {
|
|
19742
|
-
const auth = new
|
|
20070
|
+
const auth = new Command10("auth").description("Manage CLI authentication");
|
|
19743
20071
|
auth.command("login").description("Verify and store a Pretense API key (or open the browser flow)").option("--api-key <key>", "API key to store (e.g. pt-****)").option("--key <key>", "API key to store (alias for --api-key)").option(
|
|
19744
20072
|
"--offline",
|
|
19745
20073
|
"Skip server verification and store the key unverified (says so in the output)",
|
|
@@ -19758,9 +20086,9 @@ function authCommand() {
|
|
|
19758
20086
|
const isValidFormat = validPrefixes.some((prefix) => opts.apiKey.startsWith(prefix));
|
|
19759
20087
|
if (!isValidFormat) {
|
|
19760
20088
|
console.error(
|
|
19761
|
-
|
|
19762
|
-
x Invalid API key format.`) +
|
|
19763
|
-
Key must start with prtns_live_, prtns_test_, or a legacy pt-free-/pt-pro-/pt-ent- prefix`) +
|
|
20089
|
+
chalk10.red(`
|
|
20090
|
+
x Invalid API key format.`) + chalk10.dim(`
|
|
20091
|
+
Key must start with prtns_live_, prtns_test_, or a legacy pt-free-/pt-pro-/pt-ent- prefix`) + chalk10.dim(`
|
|
19764
20092
|
Try: pretense auth login (opens browser to get a valid key)
|
|
19765
20093
|
`)
|
|
19766
20094
|
);
|
|
@@ -19775,10 +20103,10 @@ function authCommand() {
|
|
|
19775
20103
|
const existing = readCredentials();
|
|
19776
20104
|
if (existing?.verified === true) {
|
|
19777
20105
|
console.error(
|
|
19778
|
-
|
|
19779
|
-
Rejected: ${maskApiKey(opts.apiKey)}`) +
|
|
19780
|
-
Kept: ${maskApiKey(existing.api_key)} (verified ${existing.verified_at ?? "?"})`) +
|
|
19781
|
-
Server: ${verification.url}`) +
|
|
20106
|
+
chalk10.red("\n x The API key was rejected \u2014 your existing key was kept.") + chalk10.dim(`
|
|
20107
|
+
Rejected: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
|
|
20108
|
+
Kept: ${maskApiKey(existing.api_key)} (verified ${existing.verified_at ?? "?"})`) + chalk10.dim(`
|
|
20109
|
+
Server: ${verification.url}`) + chalk10.dim(`
|
|
19782
20110
|
Reason: ${verification.detail}
|
|
19783
20111
|
`)
|
|
19784
20112
|
);
|
|
@@ -19793,10 +20121,10 @@ function authCommand() {
|
|
|
19793
20121
|
rejected_by: verification.url
|
|
19794
20122
|
});
|
|
19795
20123
|
console.error(
|
|
19796
|
-
|
|
19797
|
-
Key: ${maskApiKey(opts.apiKey)}`) +
|
|
19798
|
-
Server: ${verification.url}`) +
|
|
19799
|
-
Reason: ${verification.detail}`) +
|
|
20124
|
+
chalk10.red("\n x The Pretense API REJECTED this key. It will not work.") + chalk10.dim(`
|
|
20125
|
+
Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
|
|
20126
|
+
Server: ${verification.url}`) + chalk10.dim(`
|
|
20127
|
+
Reason: ${verification.detail}`) + chalk10.dim(`
|
|
19800
20128
|
Get a current key at https://pretense.ai/auth
|
|
19801
20129
|
`) + storageNotice()
|
|
19802
20130
|
);
|
|
@@ -19814,19 +20142,22 @@ function authCommand() {
|
|
|
19814
20142
|
};
|
|
19815
20143
|
writeCredentials(creds);
|
|
19816
20144
|
if (verified) {
|
|
20145
|
+
const planIsFree = String(verification.plan ?? "").trim().toLowerCase() === "free";
|
|
19817
20146
|
console.log(
|
|
19818
|
-
|
|
19819
|
-
Key: ${maskApiKey(opts.apiKey)}`) +
|
|
19820
|
-
Server: ${verification.url}`) + (verification.plan ?
|
|
19821
|
-
Plan: ${verification.plan}`) : "") +
|
|
20147
|
+
chalk10.green("\n Authenticated. The key was verified by the Pretense API.") + chalk10.dim(`
|
|
20148
|
+
Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
|
|
20149
|
+
Server: ${verification.url}`) + (verification.plan ? chalk10.dim(`
|
|
20150
|
+
Plan: ${verification.plan}`) : "") + (planIsFree ? chalk10.dim(
|
|
20151
|
+
"\n Not the plan you expected? A PRO-flagged org without an active\n subscription/comp resolves to Free \u2014 see pretense.ai/dashboard/settings."
|
|
20152
|
+
) : "") + "\n" + storageNotice()
|
|
19822
20153
|
);
|
|
19823
20154
|
return;
|
|
19824
20155
|
}
|
|
19825
20156
|
console.log(
|
|
19826
|
-
|
|
19827
|
-
Key: ${maskApiKey(opts.apiKey)}`) +
|
|
19828
|
-
Server: ${verification.url}`) +
|
|
19829
|
-
Reason: ${verification.detail}`) +
|
|
20157
|
+
chalk10.yellow("\n Key saved \u2014 NOT verified.") + chalk10.dim(`
|
|
20158
|
+
Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
|
|
20159
|
+
Server: ${verification.url}`) + chalk10.dim(`
|
|
20160
|
+
Reason: ${verification.detail}`) + chalk10.dim(
|
|
19830
20161
|
`
|
|
19831
20162
|
The key's format is valid; nothing has confirmed the key itself.
|
|
19832
20163
|
Re-run \`pretense auth login --key <key>\` when online to verify it.
|
|
@@ -19836,58 +20167,58 @@ function authCommand() {
|
|
|
19836
20167
|
return;
|
|
19837
20168
|
}
|
|
19838
20169
|
const authUrl = "https://pretense.ai/auth";
|
|
19839
|
-
console.log(
|
|
19840
|
-
console.log(
|
|
19841
|
-
console.log(
|
|
19842
|
-
console.log(
|
|
19843
|
-
console.log(
|
|
20170
|
+
console.log(chalk10.cyan("\n Opening browser to https://pretense.ai/auth"));
|
|
20171
|
+
console.log(chalk10.white(" 1. Sign in or create your account"));
|
|
20172
|
+
console.log(chalk10.white(" 2. Copy your API key from the page"));
|
|
20173
|
+
console.log(chalk10.white(" 3. Return here and run:"));
|
|
20174
|
+
console.log(chalk10.green(" pretense auth login --api-key <your-api-key>\n"));
|
|
19844
20175
|
openUrl(authUrl);
|
|
19845
20176
|
});
|
|
19846
20177
|
auth.command("logout").description("Remove stored credentials").action(() => {
|
|
19847
20178
|
if (!existsSync11(CREDENTIALS_PATH)) {
|
|
19848
|
-
console.log(
|
|
20179
|
+
console.log(chalk10.yellow("\n No credentials found. Already logged out.\n"));
|
|
19849
20180
|
return;
|
|
19850
20181
|
}
|
|
19851
20182
|
unlinkSync2(CREDENTIALS_PATH);
|
|
19852
|
-
console.log(
|
|
20183
|
+
console.log(chalk10.green("\n Logged out. Credentials removed.\n"));
|
|
19853
20184
|
});
|
|
19854
20185
|
auth.command("status").description("Show current authentication state").action(() => {
|
|
19855
20186
|
const creds = readCredentials();
|
|
19856
20187
|
if (!creds) {
|
|
19857
|
-
console.log(
|
|
20188
|
+
console.log(chalk10.yellow("\n Not authenticated."));
|
|
19858
20189
|
console.log(
|
|
19859
|
-
|
|
20190
|
+
chalk10.dim(" Run ") + chalk10.cyan("pretense auth login --api-key <key>") + chalk10.dim(" to authenticate.\n")
|
|
19860
20191
|
);
|
|
19861
20192
|
return;
|
|
19862
20193
|
}
|
|
19863
20194
|
if (creds.verified === true) {
|
|
19864
|
-
console.log(
|
|
20195
|
+
console.log(chalk10.green("\n Authenticated (key verified by the Pretense API)"));
|
|
19865
20196
|
} else if (creds.rejected_at) {
|
|
19866
|
-
console.log(
|
|
20197
|
+
console.log(chalk10.red("\n Key stored \u2014 REJECTED by the Pretense API"));
|
|
19867
20198
|
console.log(
|
|
19868
|
-
|
|
20199
|
+
chalk10.dim(` Rejected at ${creds.rejected_at} by ${creds.rejected_by ?? "?"}. `) + chalk10.dim("This key will not work; get a current one at https://pretense.ai/auth")
|
|
19869
20200
|
);
|
|
19870
20201
|
} else {
|
|
19871
|
-
console.log(
|
|
20202
|
+
console.log(chalk10.yellow("\n Key stored \u2014 NOT verified"));
|
|
19872
20203
|
console.log(
|
|
19873
|
-
|
|
20204
|
+
chalk10.dim(" No server has confirmed this key. Run ") + chalk10.cyan("pretense auth login --key <key>") + chalk10.dim(" while online to verify it.")
|
|
19874
20205
|
);
|
|
19875
20206
|
}
|
|
19876
|
-
console.log(
|
|
20207
|
+
console.log(chalk10.dim(` API Key: ${maskApiKey(creds.api_key)}`));
|
|
19877
20208
|
if (creds.authenticated_at) {
|
|
19878
|
-
console.log(
|
|
20209
|
+
console.log(chalk10.dim(` Saved: ${creds.authenticated_at}`));
|
|
19879
20210
|
}
|
|
19880
20211
|
if (creds.verified_at) {
|
|
19881
|
-
console.log(
|
|
20212
|
+
console.log(chalk10.dim(` Verified: ${creds.verified_at} by ${creds.verified_by ?? "?"}`));
|
|
19882
20213
|
}
|
|
19883
20214
|
if (creds.plan) {
|
|
19884
|
-
console.log(
|
|
20215
|
+
console.log(chalk10.dim(` Plan: ${creds.plan}`));
|
|
19885
20216
|
}
|
|
19886
|
-
console.log(
|
|
20217
|
+
console.log(chalk10.dim(` Stored: ${CREDENTIALS_PATH} (mode 0600, plaintext)`));
|
|
19887
20218
|
if (creds.expires_at) {
|
|
19888
20219
|
const expired = new Date(creds.expires_at) < /* @__PURE__ */ new Date();
|
|
19889
|
-
const label = expired ?
|
|
19890
|
-
console.log(
|
|
20220
|
+
const label = expired ? chalk10.red("Expired") : chalk10.green("Valid");
|
|
20221
|
+
console.log(chalk10.dim(` Expires: ${creds.expires_at} (${label})`));
|
|
19891
20222
|
}
|
|
19892
20223
|
console.log();
|
|
19893
20224
|
});
|
|
@@ -19896,8 +20227,8 @@ function authCommand() {
|
|
|
19896
20227
|
|
|
19897
20228
|
// src/commands/review.ts
|
|
19898
20229
|
init_esm_shims();
|
|
19899
|
-
import { Command as
|
|
19900
|
-
import
|
|
20230
|
+
import { Command as Command11 } from "commander";
|
|
20231
|
+
import chalk11 from "chalk";
|
|
19901
20232
|
import { readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
|
|
19902
20233
|
import { join as join14, extname as extname3, relative, isAbsolute } from "path";
|
|
19903
20234
|
var SUPPORTED_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
@@ -20072,37 +20403,37 @@ function formatInteractive(results) {
|
|
|
20072
20403
|
const filesWithFindings = results.filter((r) => r.mutations.length > 0 || r.secrets.length > 0);
|
|
20073
20404
|
const totalMutations = results.reduce((sum, r) => sum + r.mutations.length, 0);
|
|
20074
20405
|
const totalSecrets = results.reduce((sum, r) => sum + r.secrets.length, 0);
|
|
20075
|
-
console.log(
|
|
20076
|
-
console.log(
|
|
20406
|
+
console.log(chalk11.cyan("\n Pretense Review\n"));
|
|
20407
|
+
console.log(chalk11.dim(` Files scanned: ${results.length} | With findings: ${filesWithFindings.length}
|
|
20077
20408
|
`));
|
|
20078
20409
|
if (totalMutations === 0 && totalSecrets === 0) {
|
|
20079
|
-
console.log(
|
|
20410
|
+
console.log(chalk11.green(" No mutations or secrets detected.\n"));
|
|
20080
20411
|
return;
|
|
20081
20412
|
}
|
|
20082
|
-
console.log(
|
|
20413
|
+
console.log(chalk11.bold(` ${totalMutations} mutation(s) | ${totalSecrets} secret(s)
|
|
20083
20414
|
`));
|
|
20084
20415
|
for (const r of filesWithFindings) {
|
|
20085
|
-
console.log(
|
|
20416
|
+
console.log(chalk11.bold(` ${r.file}`));
|
|
20086
20417
|
for (const m of r.mutations) {
|
|
20087
|
-
const catColor = m.category === "function" ?
|
|
20088
|
-
console.log(` ${catColor(m.category.padEnd(10))} ${
|
|
20418
|
+
const catColor = m.category === "function" ? chalk11.blue : m.category === "class" ? chalk11.magenta : m.category === "constant" ? chalk11.yellow : chalk11.white;
|
|
20419
|
+
console.log(` ${catColor(m.category.padEnd(10))} ${chalk11.red(m.original)} ${chalk11.dim("->")} ${chalk11.green(m.mutated)} ${chalk11.dim(`L${m.line}`)}`);
|
|
20089
20420
|
}
|
|
20090
20421
|
for (const s of r.secrets) {
|
|
20091
|
-
const icon = s.severity === "critical" ?
|
|
20092
|
-
console.log(` ${icon.padEnd(10)} ${
|
|
20422
|
+
const icon = s.severity === "critical" ? chalk11.red("CRIT") : s.severity === "high" ? chalk11.yellow("HIGH") : chalk11.dim(s.severity.toUpperCase());
|
|
20423
|
+
console.log(` ${icon.padEnd(10)} ${chalk11.bold(s.type)} ${chalk11.dim(s.locator)}`);
|
|
20093
20424
|
}
|
|
20094
20425
|
console.log();
|
|
20095
20426
|
}
|
|
20096
20427
|
}
|
|
20097
20428
|
function reviewCommand() {
|
|
20098
|
-
return new
|
|
20429
|
+
return new Command11("review").description("Preview mutations and secrets that would be applied").argument("[path]", "Directory to review (defaults to current directory)").option("--plain", "Plain text output", false).option("--json", "JSON array output with metadata", false).option("--agent", "Structured JSON optimized for AI agent consumption", false).option("--prompt-only", "Minimal output suitable for piping", false).option("--interactive", "Interactive formatted output (default)", false).action(async (path2, opts) => {
|
|
20099
20430
|
const targetDir = path2 ? isAbsolute(path2) ? path2 : join14(process.cwd(), path2) : process.cwd();
|
|
20100
20431
|
const files = discoverFiles(targetDir);
|
|
20101
20432
|
if (files.length === 0) {
|
|
20102
20433
|
if (opts?.json || opts?.agent) {
|
|
20103
20434
|
console.log("[]");
|
|
20104
20435
|
} else {
|
|
20105
|
-
console.error(
|
|
20436
|
+
console.error(chalk11.yellow(" No supported source files found.\n"));
|
|
20106
20437
|
}
|
|
20107
20438
|
return;
|
|
20108
20439
|
}
|
|
@@ -20129,8 +20460,8 @@ function reviewCommand() {
|
|
|
20129
20460
|
|
|
20130
20461
|
// src/commands/install.ts
|
|
20131
20462
|
init_esm_shims();
|
|
20132
|
-
import { Command as
|
|
20133
|
-
import
|
|
20463
|
+
import { Command as Command12 } from "commander";
|
|
20464
|
+
import chalk12 from "chalk";
|
|
20134
20465
|
import {
|
|
20135
20466
|
existsSync as existsSync12,
|
|
20136
20467
|
writeFileSync as writeFileSync5,
|
|
@@ -20176,7 +20507,7 @@ function installHook(hooksDir, mode, force) {
|
|
|
20176
20507
|
return { installed: true, path: hookPath };
|
|
20177
20508
|
}
|
|
20178
20509
|
function installCommand() {
|
|
20179
|
-
return new
|
|
20510
|
+
return new Command12("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option(
|
|
20180
20511
|
"-m, --mode <mode>",
|
|
20181
20512
|
"Hook mode: pre-commit, pre-push (omit to install both)"
|
|
20182
20513
|
).option("-f, --force", "Overwrite existing hooks without prompting", false).option("-d, --dir <path>", "Project directory (defaults to cwd)", ".").action((opts) => {
|
|
@@ -20185,7 +20516,7 @@ function installCommand() {
|
|
|
20185
20516
|
const modeArg = opts.mode;
|
|
20186
20517
|
if (modeArg && !VALID_MODES.includes(modeArg)) {
|
|
20187
20518
|
console.error(
|
|
20188
|
-
|
|
20519
|
+
chalk12.red(
|
|
20189
20520
|
`
|
|
20190
20521
|
Error: Invalid mode "${modeArg}". Use: ${VALID_MODES.join(", ")}
|
|
20191
20522
|
`
|
|
@@ -20197,7 +20528,7 @@ function installCommand() {
|
|
|
20197
20528
|
const gitDir = findGitDir(dir);
|
|
20198
20529
|
if (!gitDir) {
|
|
20199
20530
|
console.error(
|
|
20200
|
-
|
|
20531
|
+
chalk12.red(
|
|
20201
20532
|
`
|
|
20202
20533
|
Error: No .git directory found in ${dir}
|
|
20203
20534
|
Run this command from a git repository root.
|
|
@@ -20208,23 +20539,23 @@ function installCommand() {
|
|
|
20208
20539
|
}
|
|
20209
20540
|
const hooksDir = join15(gitDir, "hooks");
|
|
20210
20541
|
mkdirSync3(hooksDir, { recursive: true });
|
|
20211
|
-
console.log(
|
|
20542
|
+
console.log(chalk12.cyan("\n Pretense hook installer\n"));
|
|
20212
20543
|
let hasError = false;
|
|
20213
20544
|
for (const mode of modes) {
|
|
20214
20545
|
const result = installHook(hooksDir, mode, force);
|
|
20215
20546
|
if (result.installed) {
|
|
20216
20547
|
console.log(
|
|
20217
|
-
|
|
20548
|
+
chalk12.green(` Installed ${mode} hook at ${result.path}`)
|
|
20218
20549
|
);
|
|
20219
20550
|
} else if (result.skipped) {
|
|
20220
20551
|
console.log(
|
|
20221
|
-
|
|
20552
|
+
chalk12.dim(
|
|
20222
20553
|
` Pretense ${mode} hook already installed at ${result.path}`
|
|
20223
20554
|
)
|
|
20224
20555
|
);
|
|
20225
20556
|
} else {
|
|
20226
20557
|
console.error(
|
|
20227
|
-
|
|
20558
|
+
chalk12.yellow(
|
|
20228
20559
|
` Hook already exists at ${result.path}. Use --force to overwrite.`
|
|
20229
20560
|
)
|
|
20230
20561
|
);
|
|
@@ -20235,14 +20566,14 @@ function installCommand() {
|
|
|
20235
20566
|
console.log("");
|
|
20236
20567
|
process.exit(1);
|
|
20237
20568
|
}
|
|
20238
|
-
console.log(
|
|
20569
|
+
console.log(chalk12.bold.green("\n Git hooks installed successfully.\n"));
|
|
20239
20570
|
});
|
|
20240
20571
|
}
|
|
20241
20572
|
|
|
20242
20573
|
// src/commands/ignore.ts
|
|
20243
20574
|
init_esm_shims();
|
|
20244
|
-
import { Command as
|
|
20245
|
-
import
|
|
20575
|
+
import { Command as Command13 } from "commander";
|
|
20576
|
+
import chalk13 from "chalk";
|
|
20246
20577
|
import { appendFileSync as appendFileSync3, readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
|
|
20247
20578
|
import { join as join16 } from "path";
|
|
20248
20579
|
import { homedir as homedir8 } from "os";
|
|
@@ -20261,7 +20592,7 @@ function appendPattern(pattern) {
|
|
|
20261
20592
|
const content = existsSync13(p) ? readFileSync14(p, "utf-8") : "";
|
|
20262
20593
|
const lines = content.split("\n").filter((l) => l.length > 0);
|
|
20263
20594
|
if (lines.includes(pattern)) {
|
|
20264
|
-
console.log(
|
|
20595
|
+
console.log(chalk13.yellow(`
|
|
20265
20596
|
Pattern already exists: ${pattern}
|
|
20266
20597
|
`));
|
|
20267
20598
|
return;
|
|
@@ -20269,26 +20600,26 @@ function appendPattern(pattern) {
|
|
|
20269
20600
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
20270
20601
|
appendFileSync3(p, `${prefix}${pattern}
|
|
20271
20602
|
`);
|
|
20272
|
-
console.log(
|
|
20603
|
+
console.log(chalk13.green(`
|
|
20273
20604
|
\u2713 Added to ${IGNORE_FILE}: ${pattern}
|
|
20274
20605
|
`));
|
|
20275
20606
|
}
|
|
20276
20607
|
function ignoreCommand() {
|
|
20277
|
-
return new
|
|
20608
|
+
return new Command13("ignore").description("Add patterns to .pretenseignore").argument("[pattern]", "Glob pattern to ignore").option("--last-found", "Ignore all identifiers from last scan results").option("--list", "List current ignore patterns").action((pattern, opts) => {
|
|
20278
20609
|
if (opts.list) {
|
|
20279
20610
|
const patterns = readIgnoreFile();
|
|
20280
20611
|
if (patterns.length === 0) {
|
|
20281
|
-
console.log(
|
|
20612
|
+
console.log(chalk13.dim(`
|
|
20282
20613
|
No ${IGNORE_FILE} found or file is empty.
|
|
20283
20614
|
`));
|
|
20284
20615
|
return;
|
|
20285
20616
|
}
|
|
20286
|
-
console.log(
|
|
20617
|
+
console.log(chalk13.cyan(`
|
|
20287
20618
|
${IGNORE_FILE}:
|
|
20288
20619
|
`));
|
|
20289
20620
|
for (const line of patterns) {
|
|
20290
20621
|
if (line.startsWith("#")) {
|
|
20291
|
-
console.log(
|
|
20622
|
+
console.log(chalk13.dim(` ${line}`));
|
|
20292
20623
|
} else {
|
|
20293
20624
|
console.log(` ${line}`);
|
|
20294
20625
|
}
|
|
@@ -20298,10 +20629,10 @@ function ignoreCommand() {
|
|
|
20298
20629
|
}
|
|
20299
20630
|
if (opts.lastFound) {
|
|
20300
20631
|
if (!existsSync13(LAST_SCAN_PATH)) {
|
|
20301
|
-
console.error(
|
|
20632
|
+
console.error(chalk13.red(`
|
|
20302
20633
|
\u2717 No last scan results found at ${LAST_SCAN_PATH}
|
|
20303
20634
|
`));
|
|
20304
|
-
console.error(
|
|
20635
|
+
console.error(chalk13.dim(" Run 'pretense scan <file>' first.\n"));
|
|
20305
20636
|
process.exit(1);
|
|
20306
20637
|
}
|
|
20307
20638
|
try {
|
|
@@ -20309,7 +20640,7 @@ function ignoreCommand() {
|
|
|
20309
20640
|
const data = JSON.parse(raw2);
|
|
20310
20641
|
const matches = data.matches ?? [];
|
|
20311
20642
|
if (matches.length === 0) {
|
|
20312
|
-
console.log(
|
|
20643
|
+
console.log(chalk13.green("\n \u2713 Last scan was clean - nothing to ignore.\n"));
|
|
20313
20644
|
return;
|
|
20314
20645
|
}
|
|
20315
20646
|
let added = 0;
|
|
@@ -20320,11 +20651,11 @@ function ignoreCommand() {
|
|
|
20320
20651
|
added++;
|
|
20321
20652
|
}
|
|
20322
20653
|
}
|
|
20323
|
-
console.log(
|
|
20654
|
+
console.log(chalk13.green(`
|
|
20324
20655
|
\u2713 Added ${added} pattern(s) from last scan.
|
|
20325
20656
|
`));
|
|
20326
20657
|
} catch {
|
|
20327
|
-
console.error(
|
|
20658
|
+
console.error(chalk13.red(`
|
|
20328
20659
|
\u2717 Failed to parse ${LAST_SCAN_PATH}
|
|
20329
20660
|
`));
|
|
20330
20661
|
process.exit(1);
|
|
@@ -20332,7 +20663,7 @@ function ignoreCommand() {
|
|
|
20332
20663
|
return;
|
|
20333
20664
|
}
|
|
20334
20665
|
if (!pattern) {
|
|
20335
|
-
console.error(
|
|
20666
|
+
console.error(chalk13.red("\n \u2717 Please provide a pattern or use --last-found\n"));
|
|
20336
20667
|
process.exit(1);
|
|
20337
20668
|
}
|
|
20338
20669
|
appendPattern(pattern);
|
|
@@ -20341,7 +20672,7 @@ function ignoreCommand() {
|
|
|
20341
20672
|
|
|
20342
20673
|
// src/commands/completion.ts
|
|
20343
20674
|
init_esm_shims();
|
|
20344
|
-
import { Command as
|
|
20675
|
+
import { Command as Command14 } from "commander";
|
|
20345
20676
|
var ZSH_COMPLETION = `
|
|
20346
20677
|
# Pretense zsh completion
|
|
20347
20678
|
_pretense() {
|
|
@@ -20533,7 +20864,7 @@ complete -c pretense -f -n "__fish_seen_subcommand_from completion" -a bash -d "
|
|
|
20533
20864
|
complete -c pretense -f -n "__fish_seen_subcommand_from completion" -a fish -d "fish completion script"
|
|
20534
20865
|
`.trim();
|
|
20535
20866
|
function completionCommand() {
|
|
20536
|
-
return new
|
|
20867
|
+
return new Command14("completion").description("Print shell tab-completion script").argument("<shell>", "Shell to generate completion for: zsh, bash, fish").action((shell) => {
|
|
20537
20868
|
switch (shell) {
|
|
20538
20869
|
case "zsh":
|
|
20539
20870
|
console.log(ZSH_COMPLETION);
|
|
@@ -20553,8 +20884,8 @@ function completionCommand() {
|
|
|
20553
20884
|
|
|
20554
20885
|
// src/commands/quickstart.ts
|
|
20555
20886
|
init_esm_shims();
|
|
20556
|
-
import { Command as
|
|
20557
|
-
import
|
|
20887
|
+
import { Command as Command15 } from "commander";
|
|
20888
|
+
import chalk14 from "chalk";
|
|
20558
20889
|
import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "fs";
|
|
20559
20890
|
import { join as join17, resolve as resolve4 } from "path";
|
|
20560
20891
|
var DEFAULT_CONFIG_YAML = `scan:
|
|
@@ -20582,60 +20913,62 @@ customPatterns: []
|
|
|
20582
20913
|
verbose: false
|
|
20583
20914
|
`;
|
|
20584
20915
|
function quickstartCommand() {
|
|
20585
|
-
return new
|
|
20916
|
+
return new Command15("quickstart").description("One-command setup: init + install hooks + show next steps").option("-d, --dir <path>", "Directory to scan", ".").option("--dry-run", "Print what would be written without writing", false).action(async (opts) => {
|
|
20586
20917
|
const dir = resolve4(opts.dir);
|
|
20587
|
-
console.log(
|
|
20588
|
-
console.log(
|
|
20589
|
-
console.log(
|
|
20918
|
+
console.log(chalk14.bold.cyan("\n Pretense Quickstart\n"));
|
|
20919
|
+
console.log(chalk14.dim(" Setting up AI firewall for this project...\n"));
|
|
20920
|
+
console.log(chalk14.dim(" [1/4] Scanning codebase..."));
|
|
20590
20921
|
let fp;
|
|
20591
20922
|
try {
|
|
20592
20923
|
fp = await learn({ dir, minOccurrences: 1 });
|
|
20593
|
-
console.log(
|
|
20924
|
+
console.log(chalk14.green(` ${fp.filesScanned} files | ${fp.functions.length} functions | ${fp.classes.length} classes`));
|
|
20594
20925
|
} catch (err) {
|
|
20595
|
-
console.error(
|
|
20926
|
+
console.error(chalk14.red(` \u2717 Scan failed: ${err.message}`));
|
|
20596
20927
|
process.exit(1);
|
|
20597
20928
|
}
|
|
20598
|
-
console.log(
|
|
20929
|
+
console.log(chalk14.dim(" [2/4] Writing config..."));
|
|
20599
20930
|
const configPath = join17(dir, "pretense.yaml");
|
|
20600
20931
|
if (!opts.dryRun) {
|
|
20601
20932
|
if (!existsSync14(configPath)) {
|
|
20602
20933
|
writeFileSync6(configPath, DEFAULT_CONFIG_YAML);
|
|
20603
|
-
console.log(
|
|
20934
|
+
console.log(chalk14.green(" Created pretense.yaml"));
|
|
20604
20935
|
} else {
|
|
20605
|
-
console.log(
|
|
20936
|
+
console.log(chalk14.dim(" pretense.yaml already exists, skipping"));
|
|
20606
20937
|
}
|
|
20607
20938
|
}
|
|
20608
|
-
console.log(
|
|
20939
|
+
console.log(chalk14.dim(" [3/4] Updating .gitignore..."));
|
|
20609
20940
|
if (!opts.dryRun) {
|
|
20610
20941
|
const gitignorePath = join17(dir, ".gitignore");
|
|
20611
20942
|
let content = existsSync14(gitignorePath) ? (await import("fs")).default.readFileSync(gitignorePath, "utf-8") : "";
|
|
20612
20943
|
if (!content.includes(".pretense/")) {
|
|
20613
20944
|
content = content + (content.endsWith("\n") ? "" : "\n") + ".pretense/\n";
|
|
20614
20945
|
writeFileSync6(gitignorePath, content);
|
|
20615
|
-
console.log(
|
|
20946
|
+
console.log(chalk14.green(" Added .pretense/ to .gitignore"));
|
|
20616
20947
|
} else {
|
|
20617
|
-
console.log(
|
|
20618
|
-
}
|
|
20619
|
-
}
|
|
20620
|
-
console.log(
|
|
20621
|
-
console.log(
|
|
20622
|
-
console.log(` ${
|
|
20623
|
-
console.log(` ${
|
|
20624
|
-
console.log(`
|
|
20625
|
-
console.log(` ${
|
|
20626
|
-
console.log(`
|
|
20627
|
-
console.log(` ${
|
|
20628
|
-
console.log(` ${
|
|
20629
|
-
console.log(` ${
|
|
20948
|
+
console.log(chalk14.dim(" .gitignore already includes .pretense/"));
|
|
20949
|
+
}
|
|
20950
|
+
}
|
|
20951
|
+
console.log(chalk14.dim(" [4/4] Done.\n"));
|
|
20952
|
+
console.log(chalk14.bold(" Next steps:\n"));
|
|
20953
|
+
console.log(` ${chalk14.green("1.")} ${chalk14.cyan('pretense run claude "build me X"')} Run any AI tool through the proxy \u2014 zero setup`);
|
|
20954
|
+
console.log(` ${chalk14.dim("(auto-starts the proxy on a free port and sets the base-URL env var for you)")}`);
|
|
20955
|
+
console.log(` ${chalk14.dim("Works the same for any tool: ")}${chalk14.cyan("pretense run cursor")}${chalk14.dim(", ")}${chalk14.cyan("pretense run gemini")}`);
|
|
20956
|
+
console.log(` ${chalk14.green("2.")} ${chalk14.cyan("pretense install")} Install pre-commit + pre-push git hooks`);
|
|
20957
|
+
console.log(` ${chalk14.green("3.")} ${chalk14.dim("Prefer to wire it by hand? Set the base URL in the SAME command as the tool:")}`);
|
|
20958
|
+
console.log(` ${chalk14.dim("ANTHROPIC_BASE_URL=http://localhost:9339 claude")}`);
|
|
20959
|
+
console.log(` ${chalk14.dim("OPENAI_BASE_URL=http://localhost:9339/v1 cursor ...")}`);
|
|
20960
|
+
console.log(` ${chalk14.dim("GOOGLE_GEMINI_BASE_URL=http://localhost:9339 gemini ...")}`);
|
|
20961
|
+
console.log(` ${chalk14.bold.yellow("\u26A0 Set this in the SAME terminal as your AI tool \u2014 a new terminal is NOT protected.")}`);
|
|
20962
|
+
console.log(` ${chalk14.bold.yellow(" Simplest: use ")}${chalk14.cyan("pretense run")}${chalk14.bold.yellow(" and skip the export entirely.")}
|
|
20630
20963
|
`);
|
|
20631
|
-
console.log(
|
|
20964
|
+
console.log(chalk14.bold.green(" Protected. Happy coding.\n"));
|
|
20632
20965
|
});
|
|
20633
20966
|
}
|
|
20634
20967
|
|
|
20635
20968
|
// src/commands/mutate.ts
|
|
20636
20969
|
init_esm_shims();
|
|
20637
|
-
import { Command as
|
|
20638
|
-
import
|
|
20970
|
+
import { Command as Command16, Option as Option2 } from "commander";
|
|
20971
|
+
import chalk16 from "chalk";
|
|
20639
20972
|
import { readFileSync as readFileSync15, writeFileSync as writeFileSync7, chmodSync as chmodSync4, existsSync as existsSync15 } from "fs";
|
|
20640
20973
|
import { createHash as createHash3 } from "crypto";
|
|
20641
20974
|
import { resolve as resolve5, extname as extname4 } from "path";
|
|
@@ -20761,6 +21094,48 @@ function ensureDefaultMapPath(absFilePath, override2) {
|
|
|
20761
21094
|
return defaultMapPath(absFilePath, override2);
|
|
20762
21095
|
}
|
|
20763
21096
|
|
|
21097
|
+
// src/local-audit.ts
|
|
21098
|
+
init_esm_shims();
|
|
21099
|
+
import { homedir as homedir10 } from "os";
|
|
21100
|
+
import { join as join21 } from "path";
|
|
21101
|
+
import { randomUUID } from "crypto";
|
|
21102
|
+
import chalk15 from "chalk";
|
|
21103
|
+
function localAuditDbPath() {
|
|
21104
|
+
return join21(homedir10(), ".pretense", "audit.db");
|
|
21105
|
+
}
|
|
21106
|
+
function recordLocalAudit(rec, dbPath = localAuditDbPath()) {
|
|
21107
|
+
let store = null;
|
|
21108
|
+
try {
|
|
21109
|
+
store = new AuditStore(dbPath);
|
|
21110
|
+
store.logEntry({
|
|
21111
|
+
timestamp: Date.now(),
|
|
21112
|
+
requestId: randomUUID(),
|
|
21113
|
+
provider: "cli",
|
|
21114
|
+
model: "-",
|
|
21115
|
+
direction: "outbound",
|
|
21116
|
+
originalSize: rec.originalSize,
|
|
21117
|
+
processedSize: rec.processedSize,
|
|
21118
|
+
secretsBlocked: rec.secretsBlocked,
|
|
21119
|
+
piiRedacted: rec.piiRedacted,
|
|
21120
|
+
mutationsApplied: rec.mutationsApplied,
|
|
21121
|
+
scanDurationMs: 0,
|
|
21122
|
+
mutationDurationMs: rec.durationMs,
|
|
21123
|
+
action: rec.action,
|
|
21124
|
+
details: { source: rec.action === "reversed" ? "cli-reverse" : "cli-mutate", file: rec.file },
|
|
21125
|
+
...rec.detectorKinds && rec.detectorKinds.length > 0 ? { detectorKinds: rec.detectorKinds } : {}
|
|
21126
|
+
});
|
|
21127
|
+
return true;
|
|
21128
|
+
} catch (err) {
|
|
21129
|
+
console.error(
|
|
21130
|
+
chalk15.yellow(` ! Could not record this ${rec.action === "reversed" ? "reversal" : "mutation"} in the audit log`) + chalk15.dim(` (${err.message}).`) + chalk15.dim(`
|
|
21131
|
+
The file was still ${rec.action === "reversed" ? "restored" : "mutated"}; only the audit entry is missing.`)
|
|
21132
|
+
);
|
|
21133
|
+
return false;
|
|
21134
|
+
} finally {
|
|
21135
|
+
store?.close();
|
|
21136
|
+
}
|
|
21137
|
+
}
|
|
21138
|
+
|
|
20764
21139
|
// src/commands/mutate.ts
|
|
20765
21140
|
var REVERSAL_MAP_VERSION = 3;
|
|
20766
21141
|
function preflightReversalMap(mapPath, absFile, content, contentSha, force) {
|
|
@@ -20857,8 +21232,10 @@ async function collectMutationFindings(content, file, level, presetName) {
|
|
|
20857
21232
|
const cannotMutate = (type) => {
|
|
20858
21233
|
unmutatable.set(type, (unmutatable.get(type) ?? 0) + 1);
|
|
20859
21234
|
};
|
|
21235
|
+
const categoryByType = /* @__PURE__ */ new Map();
|
|
20860
21236
|
for (const m of result.matches) {
|
|
20861
21237
|
if (level === 2 && m.level !== 2) continue;
|
|
21238
|
+
if (!categoryByType.has(m.type)) categoryByType.set(m.type, m.category);
|
|
20862
21239
|
const offset = m.start;
|
|
20863
21240
|
const length = m.end - m.start;
|
|
20864
21241
|
if (length <= 0) {
|
|
@@ -20871,18 +21248,18 @@ async function collectMutationFindings(content, file, level, presetName) {
|
|
|
20871
21248
|
}
|
|
20872
21249
|
findings.push({ value: m.value, offset, length, type: m.type });
|
|
20873
21250
|
}
|
|
20874
|
-
return { findings, unmutatable };
|
|
21251
|
+
return { findings, unmutatable, categoryByType };
|
|
20875
21252
|
}
|
|
20876
21253
|
function reportUnmutatable(unmutatable) {
|
|
20877
21254
|
if (unmutatable.size === 0) return;
|
|
20878
21255
|
const total = [...unmutatable.values()].reduce((a, b) => a + b, 0);
|
|
20879
21256
|
const kinds = [...unmutatable.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([type, n]) => n > 1 ? `${type} (x${n})` : type).join(", ");
|
|
20880
21257
|
console.error(
|
|
20881
|
-
|
|
20882
|
-
! ${total} detected value${total === 1 ? "" : "s"} could NOT be mutated: ${kinds}`) +
|
|
20883
|
-
These were found only inside an encoded form (base64/hex) or at a span`) +
|
|
20884
|
-
that does not map back to the file's bytes, so there is no range to`) +
|
|
20885
|
-
rewrite. They are STILL PRESENT in the output. Run \`pretense scan\` on`) +
|
|
21258
|
+
chalk16.yellow(`
|
|
21259
|
+
! ${total} detected value${total === 1 ? "" : "s"} could NOT be mutated: ${kinds}`) + chalk16.dim(`
|
|
21260
|
+
These were found only inside an encoded form (base64/hex) or at a span`) + chalk16.dim(`
|
|
21261
|
+
that does not map back to the file's bytes, so there is no range to`) + chalk16.dim(`
|
|
21262
|
+
rewrite. They are STILL PRESENT in the output. Run \`pretense scan\` on`) + chalk16.dim(`
|
|
20886
21263
|
this file to see them, and remove or re-encode them by hand.
|
|
20887
21264
|
`)
|
|
20888
21265
|
);
|
|
@@ -20892,9 +21269,9 @@ function writeReversalMap(mapPath, mapFile) {
|
|
|
20892
21269
|
writeFileSync7(mapPath, JSON.stringify(mapFile, null, 2), { mode: 384 });
|
|
20893
21270
|
} catch (e) {
|
|
20894
21271
|
console.error(
|
|
20895
|
-
|
|
20896
|
-
x Cannot write reversal map: ${mapPath}`) +
|
|
20897
|
-
${e.message}`) +
|
|
21272
|
+
chalk16.red(`
|
|
21273
|
+
x Cannot write reversal map: ${mapPath}`) + chalk16.dim(`
|
|
21274
|
+
${e.message}`) + chalk16.dim(`
|
|
20898
21275
|
Nothing was mutated \u2014 without a map the original could not be restored.
|
|
20899
21276
|
`)
|
|
20900
21277
|
);
|
|
@@ -20905,7 +21282,7 @@ function writeReversalMap(mapPath, mapFile) {
|
|
|
20905
21282
|
} catch {
|
|
20906
21283
|
}
|
|
20907
21284
|
console.error(
|
|
20908
|
-
|
|
21285
|
+
chalk16.green(` \u2713 Wrote encrypted reversal map \u2192 ${mapPath}`) + chalk16.dim(" (AES-256-GCM, mode 0600)")
|
|
20909
21286
|
);
|
|
20910
21287
|
return 0;
|
|
20911
21288
|
}
|
|
@@ -20914,7 +21291,7 @@ async function runMutate(file, opts = {}) {
|
|
|
20914
21291
|
try {
|
|
20915
21292
|
raw2 = readFileSync15(file);
|
|
20916
21293
|
} catch {
|
|
20917
|
-
console.error(
|
|
21294
|
+
console.error(chalk16.red(`
|
|
20918
21295
|
x Cannot read file: ${file}
|
|
20919
21296
|
`));
|
|
20920
21297
|
return 1;
|
|
@@ -20922,9 +21299,9 @@ async function runMutate(file, opts = {}) {
|
|
|
20922
21299
|
const content = raw2.toString("utf-8");
|
|
20923
21300
|
if (!Buffer.from(content, "utf-8").equals(raw2)) {
|
|
20924
21301
|
console.error(
|
|
20925
|
-
|
|
20926
|
-
x Not valid UTF-8: ${file}`) +
|
|
20927
|
-
Mutating would replace the undecodable bytes with U+FFFD and the`) +
|
|
21302
|
+
chalk16.red(`
|
|
21303
|
+
x Not valid UTF-8: ${file}`) + chalk16.dim(`
|
|
21304
|
+
Mutating would replace the undecodable bytes with U+FFFD and the`) + chalk16.dim(`
|
|
20928
21305
|
original content could not be restored. File left untouched.
|
|
20929
21306
|
`)
|
|
20930
21307
|
);
|
|
@@ -20932,7 +21309,8 @@ async function runMutate(file, opts = {}) {
|
|
|
20932
21309
|
}
|
|
20933
21310
|
const preview = opts.stdout === true;
|
|
20934
21311
|
const inPlace = !preview && !opts.output;
|
|
20935
|
-
const
|
|
21312
|
+
const mutateStartMs = Date.now();
|
|
21313
|
+
const { findings, unmutatable, categoryByType } = await collectMutationFindings(
|
|
20936
21314
|
content,
|
|
20937
21315
|
file,
|
|
20938
21316
|
opts.level,
|
|
@@ -20940,7 +21318,7 @@ async function runMutate(file, opts = {}) {
|
|
|
20940
21318
|
);
|
|
20941
21319
|
if (findings.length === 0) {
|
|
20942
21320
|
if (unmutatable.size === 0) {
|
|
20943
|
-
console.error(
|
|
21321
|
+
console.error(chalk16.dim(` No secrets found in ${file} \u2014 nothing to mutate.`));
|
|
20944
21322
|
} else {
|
|
20945
21323
|
reportUnmutatable(unmutatable);
|
|
20946
21324
|
}
|
|
@@ -20964,9 +21342,9 @@ async function runMutate(file, opts = {}) {
|
|
|
20964
21342
|
const pre = preflightReversalMap(effectiveMap, absFile, content, contentSha, opts.forceMap === true);
|
|
20965
21343
|
if (pre.action === "refuse") {
|
|
20966
21344
|
console.error(
|
|
20967
|
-
|
|
20968
|
-
x ${pre.code}: refusing to overwrite an existing reversal map.`) +
|
|
20969
|
-
${pre.message}`) +
|
|
21345
|
+
chalk16.red(`
|
|
21346
|
+
x ${pre.code}: refusing to overwrite an existing reversal map.`) + chalk16.dim(`
|
|
21347
|
+
${pre.message}`) + chalk16.dim(`
|
|
20970
21348
|
Nothing was mutated. Both the file and the map are untouched.
|
|
20971
21349
|
`)
|
|
20972
21350
|
);
|
|
@@ -20976,8 +21354,8 @@ async function runMutate(file, opts = {}) {
|
|
|
20976
21354
|
const isNoop = pre.action === "keep" && pre.expectedMutatedSha === mutatedSha;
|
|
20977
21355
|
if (isNoop) {
|
|
20978
21356
|
console.error(
|
|
20979
|
-
|
|
20980
|
-
hash \u2014 left unchanged (mutation is deterministic, so a rewrite would be`) +
|
|
21357
|
+
chalk16.dim(` = Reversal map ${effectiveMap} already describes ${file} at this exact content`) + chalk16.dim(`
|
|
21358
|
+
hash \u2014 left unchanged (mutation is deterministic, so a rewrite would be`) + chalk16.dim(`
|
|
20981
21359
|
byte-identical).`)
|
|
20982
21360
|
);
|
|
20983
21361
|
} else {
|
|
@@ -21003,10 +21381,10 @@ async function runMutate(file, opts = {}) {
|
|
|
21003
21381
|
if (written !== 0) return written;
|
|
21004
21382
|
if (pre.action === "write" && pre.generation > 1) {
|
|
21005
21383
|
console.error(
|
|
21006
|
-
|
|
21007
|
-
! --force-map: this map is mutation GENERATION ${pre.generation}.`) +
|
|
21008
|
-
Its "original" values are synthetics from the previous round, so`) +
|
|
21009
|
-
reversing with it returns the EARLIER MUTATED file, not your real`) +
|
|
21384
|
+
chalk16.yellow(`
|
|
21385
|
+
! --force-map: this map is mutation GENERATION ${pre.generation}.`) + chalk16.dim(`
|
|
21386
|
+
Its "original" values are synthetics from the previous round, so`) + chalk16.dim(`
|
|
21387
|
+
reversing with it returns the EARLIER MUTATED file, not your real`) + chalk16.dim(`
|
|
21010
21388
|
secrets. \`pretense reverse\` will refuse it unless --allow-partial.
|
|
21011
21389
|
`)
|
|
21012
21390
|
);
|
|
@@ -21015,8 +21393,8 @@ async function runMutate(file, opts = {}) {
|
|
|
21015
21393
|
}
|
|
21016
21394
|
if (inPlace && !effectiveMap) {
|
|
21017
21395
|
console.error(
|
|
21018
|
-
|
|
21019
|
-
x Refusing to mutate ${file} in place with no reversal map \u2014 this would be irreversible.`) +
|
|
21396
|
+
chalk16.red(`
|
|
21397
|
+
x Refusing to mutate ${file} in place with no reversal map \u2014 this would be irreversible.`) + chalk16.dim(`
|
|
21020
21398
|
Pass --map <path>, or report this as a bug.
|
|
21021
21399
|
`)
|
|
21022
21400
|
);
|
|
@@ -21025,29 +21403,48 @@ async function runMutate(file, opts = {}) {
|
|
|
21025
21403
|
try {
|
|
21026
21404
|
if (inPlace) {
|
|
21027
21405
|
writeFileAtomicSync(file, result.content);
|
|
21028
|
-
console.error(
|
|
21406
|
+
console.error(chalk16.green(` \u2713 Mutated ${result.mutations.length} secrets in ${file} (in-place)`));
|
|
21029
21407
|
} else if (opts.output) {
|
|
21030
21408
|
writeFileAtomicSync(opts.output, result.content);
|
|
21031
|
-
console.error(
|
|
21409
|
+
console.error(chalk16.green(` \u2713 Mutated ${result.mutations.length} secrets \u2192 ${opts.output}`));
|
|
21032
21410
|
} else {
|
|
21033
21411
|
process.stdout.write(result.content);
|
|
21034
21412
|
}
|
|
21035
21413
|
} catch (e) {
|
|
21036
21414
|
console.error(
|
|
21037
|
-
|
|
21038
|
-
x Cannot write mutated content: ${opts.output ?? file}`) +
|
|
21415
|
+
chalk16.red(`
|
|
21416
|
+
x Cannot write mutated content: ${opts.output ?? file}`) + chalk16.dim(`
|
|
21039
21417
|
${e.message}
|
|
21040
21418
|
`)
|
|
21041
21419
|
);
|
|
21042
21420
|
return 1;
|
|
21043
21421
|
}
|
|
21422
|
+
if (!preview && result.mutations.length > 0) {
|
|
21423
|
+
let secretsBlocked = 0;
|
|
21424
|
+
let piiRedacted = 0;
|
|
21425
|
+
for (const m of result.mutations) {
|
|
21426
|
+
if (categoryByType.get(m.type) === "pii") piiRedacted += 1;
|
|
21427
|
+
else secretsBlocked += 1;
|
|
21428
|
+
}
|
|
21429
|
+
recordLocalAudit({
|
|
21430
|
+
action: "mutated",
|
|
21431
|
+
mutationsApplied: result.mutations.length,
|
|
21432
|
+
secretsBlocked,
|
|
21433
|
+
piiRedacted,
|
|
21434
|
+
originalSize: Buffer.byteLength(content, "utf-8"),
|
|
21435
|
+
processedSize: Buffer.byteLength(result.content, "utf-8"),
|
|
21436
|
+
durationMs: Date.now() - mutateStartMs,
|
|
21437
|
+
detectorKinds: [...new Set(result.mutations.map((m) => m.type))],
|
|
21438
|
+
file: absFile
|
|
21439
|
+
});
|
|
21440
|
+
}
|
|
21044
21441
|
if (opts.showMap) {
|
|
21045
21442
|
console.error(JSON.stringify(result.mutations, null, 2));
|
|
21046
21443
|
}
|
|
21047
21444
|
return 0;
|
|
21048
21445
|
}
|
|
21049
21446
|
function mutateCommand() {
|
|
21050
|
-
return new
|
|
21447
|
+
return new Command16("mutate").description(
|
|
21051
21448
|
"Mutate secrets IN PLACE and auto-write an encrypted reversal map (reverse with `pretense reverse <file>`; --stdout to preview instead)"
|
|
21052
21449
|
).argument("<file>", "File to mutate").option("-i, --in-place", "Write back to the input file (this is the default; kept for compatibility)").option("-o, --output <path>", "Write mutated content to a specific file instead of in place").option("--stdout", "Preview mutated content on stdout and write NOTHING (no file, no map)").option(
|
|
21053
21450
|
"--map <path>",
|
|
@@ -21068,7 +21465,7 @@ function mutateCommand() {
|
|
|
21068
21465
|
const resolved = resolvePolicy(opts.policy);
|
|
21069
21466
|
if (resolved?.scannerPreset) opts.presetName = resolved.scannerPreset;
|
|
21070
21467
|
} catch (err) {
|
|
21071
|
-
console.error(
|
|
21468
|
+
console.error(chalk16.red(`
|
|
21072
21469
|
x ${err.message}
|
|
21073
21470
|
`));
|
|
21074
21471
|
process.exit(1);
|
|
@@ -21080,8 +21477,8 @@ function mutateCommand() {
|
|
|
21080
21477
|
|
|
21081
21478
|
// src/commands/reverse.ts
|
|
21082
21479
|
init_esm_shims();
|
|
21083
|
-
import { Command as
|
|
21084
|
-
import
|
|
21480
|
+
import { Command as Command17 } from "commander";
|
|
21481
|
+
import chalk17 from "chalk";
|
|
21085
21482
|
import { readFileSync as readFileSync16, existsSync as existsSync16, statSync as statSync6 } from "fs";
|
|
21086
21483
|
import { resolve as resolve6 } from "path";
|
|
21087
21484
|
import { createHash as createHash4 } from "crypto";
|
|
@@ -21115,14 +21512,14 @@ function loadReversalMap(mapPath) {
|
|
|
21115
21512
|
try {
|
|
21116
21513
|
parsed = JSON.parse(readFileSync16(mapPath, "utf-8"));
|
|
21117
21514
|
} catch {
|
|
21118
|
-
console.error(
|
|
21515
|
+
console.error(chalk17.red(`
|
|
21119
21516
|
x Cannot read reversal map: ${mapPath}
|
|
21120
21517
|
`));
|
|
21121
21518
|
process.exit(1);
|
|
21122
21519
|
}
|
|
21123
21520
|
const map = parsed;
|
|
21124
21521
|
if (!map || !Array.isArray(map.mutations)) {
|
|
21125
|
-
console.error(
|
|
21522
|
+
console.error(chalk17.red(`
|
|
21126
21523
|
x Malformed reversal map (missing "mutations"): ${mapPath}
|
|
21127
21524
|
`));
|
|
21128
21525
|
process.exit(1);
|
|
@@ -21130,9 +21527,9 @@ function loadReversalMap(mapPath) {
|
|
|
21130
21527
|
const version = typeof map.version === "number" ? map.version : 1;
|
|
21131
21528
|
if (version > REVERSAL_MAP_VERSION) {
|
|
21132
21529
|
console.error(
|
|
21133
|
-
|
|
21134
|
-
x Reversal map is version ${version}; this build understands up to ${REVERSAL_MAP_VERSION}.`) +
|
|
21135
|
-
It was written by a newer Pretense. Upgrade the CLI to reverse it.`) +
|
|
21530
|
+
chalk17.red(`
|
|
21531
|
+
x Reversal map is version ${version}; this build understands up to ${REVERSAL_MAP_VERSION}.`) + chalk17.dim(`
|
|
21532
|
+
It was written by a newer Pretense. Upgrade the CLI to reverse it.`) + chalk17.dim(`
|
|
21136
21533
|
File left untouched.
|
|
21137
21534
|
`)
|
|
21138
21535
|
);
|
|
@@ -21141,9 +21538,10 @@ function loadReversalMap(mapPath) {
|
|
|
21141
21538
|
return map;
|
|
21142
21539
|
}
|
|
21143
21540
|
function runReverse(file, opts = {}) {
|
|
21541
|
+
const reverseStartMs = Date.now();
|
|
21144
21542
|
const absPath = resolve6(file);
|
|
21145
21543
|
if (!existsSync16(absPath) || !statSync6(absPath).isFile()) {
|
|
21146
|
-
console.error(
|
|
21544
|
+
console.error(chalk17.red(`
|
|
21147
21545
|
x File not found: ${file}
|
|
21148
21546
|
`));
|
|
21149
21547
|
return 1;
|
|
@@ -21151,11 +21549,11 @@ function runReverse(file, opts = {}) {
|
|
|
21151
21549
|
const mapPath = opts.map ?? defaultMapPath(absPath, opts.mapsDir);
|
|
21152
21550
|
if (!opts.map && !existsSync16(mapPath)) {
|
|
21153
21551
|
console.error(
|
|
21154
|
-
|
|
21155
|
-
x No reversal map found for ${absPath}`) +
|
|
21156
|
-
Looked in the default location: ${mapPath}`) +
|
|
21157
|
-
Mutate the file first with ${
|
|
21158
|
-
an explicit ${
|
|
21552
|
+
chalk17.red(`
|
|
21553
|
+
x No reversal map found for ${absPath}`) + chalk17.dim(`
|
|
21554
|
+
Looked in the default location: ${mapPath}`) + chalk17.dim(`
|
|
21555
|
+
Mutate the file first with ${chalk17.cyan(`pretense mutate ${file}`)}, or pass`) + chalk17.dim(`
|
|
21556
|
+
an explicit ${chalk17.cyan("--map <path>")} if you wrote the map somewhere else.
|
|
21159
21557
|
`)
|
|
21160
21558
|
);
|
|
21161
21559
|
return 1;
|
|
@@ -21167,8 +21565,8 @@ function runReverse(file, opts = {}) {
|
|
|
21167
21565
|
const key = readProjectKey(projectRoot, opts.keysDir);
|
|
21168
21566
|
if (!key) {
|
|
21169
21567
|
console.error(
|
|
21170
|
-
|
|
21171
|
-
x Encryption key not found for this project \u2014 cannot decrypt the reversal map.`) +
|
|
21568
|
+
chalk17.red(`
|
|
21569
|
+
x Encryption key not found for this project \u2014 cannot decrypt the reversal map.`) + chalk17.dim(`
|
|
21172
21570
|
Expected a key derived from ${projectRoot}. Reverse must run where the map was created.
|
|
21173
21571
|
`)
|
|
21174
21572
|
);
|
|
@@ -21181,7 +21579,7 @@ function runReverse(file, opts = {}) {
|
|
|
21181
21579
|
}));
|
|
21182
21580
|
} catch {
|
|
21183
21581
|
console.error(
|
|
21184
|
-
|
|
21582
|
+
chalk17.red(`
|
|
21185
21583
|
x Failed to decrypt the reversal map (wrong key or corrupt data). File left untouched.
|
|
21186
21584
|
`)
|
|
21187
21585
|
);
|
|
@@ -21195,10 +21593,10 @@ function runReverse(file, opts = {}) {
|
|
|
21195
21593
|
const absentCount = plainMutations.length - appliedCount;
|
|
21196
21594
|
if (typeof mapFile.mutatedSha === "string" && mapFile.mutatedSha !== sha256Hex3(mutatedContent)) {
|
|
21197
21595
|
console.error(
|
|
21198
|
-
|
|
21199
|
-
! This file is not the exact output this map was written for.`) +
|
|
21200
|
-
map expects sha256 ${mapFile.mutatedSha}`) +
|
|
21201
|
-
file is sha256 ${sha256Hex3(mutatedContent)}`) +
|
|
21596
|
+
chalk17.yellow(`
|
|
21597
|
+
! This file is not the exact output this map was written for.`) + chalk17.dim(`
|
|
21598
|
+
map expects sha256 ${mapFile.mutatedSha}`) + chalk17.dim(`
|
|
21599
|
+
file is sha256 ${sha256Hex3(mutatedContent)}`) + chalk17.dim(`
|
|
21202
21600
|
${absentCount} of ${plainMutations.length} mapped tokens are not present in it.
|
|
21203
21601
|
`)
|
|
21204
21602
|
);
|
|
@@ -21208,8 +21606,8 @@ function runReverse(file, opts = {}) {
|
|
|
21208
21606
|
restored = applyReverse(mutatedContent, plainMutations);
|
|
21209
21607
|
} catch (e) {
|
|
21210
21608
|
console.error(
|
|
21211
|
-
|
|
21212
|
-
x ${e.message}`) +
|
|
21609
|
+
chalk17.red(`
|
|
21610
|
+
x ${e.message}`) + chalk17.dim(`
|
|
21213
21611
|
File left untouched.
|
|
21214
21612
|
`)
|
|
21215
21613
|
);
|
|
@@ -21217,8 +21615,8 @@ function runReverse(file, opts = {}) {
|
|
|
21217
21615
|
}
|
|
21218
21616
|
if (!mapFile.originalSha) {
|
|
21219
21617
|
console.error(
|
|
21220
|
-
|
|
21221
|
-
x Reversal map has no originalSha \u2014 cannot verify round-trip integrity.`) +
|
|
21618
|
+
chalk17.red(`
|
|
21619
|
+
x Reversal map has no originalSha \u2014 cannot verify round-trip integrity.`) + chalk17.dim(`
|
|
21222
21620
|
Refusing to write. File left untouched.
|
|
21223
21621
|
`)
|
|
21224
21622
|
);
|
|
@@ -21227,10 +21625,10 @@ function runReverse(file, opts = {}) {
|
|
|
21227
21625
|
const restoredSha = sha256Hex3(restored);
|
|
21228
21626
|
if (restoredSha !== mapFile.originalSha) {
|
|
21229
21627
|
console.error(
|
|
21230
|
-
|
|
21231
|
-
x Reversal integrity check failed for ${absPath}`) +
|
|
21232
|
-
expected sha256 ${mapFile.originalSha}`) +
|
|
21233
|
-
got sha256 ${restoredSha}`) +
|
|
21628
|
+
chalk17.red(`
|
|
21629
|
+
x Reversal integrity check failed for ${absPath}`) + chalk17.dim(`
|
|
21630
|
+
expected sha256 ${mapFile.originalSha}`) + chalk17.dim(`
|
|
21631
|
+
got sha256 ${restoredSha}`) + chalk17.dim(`
|
|
21234
21632
|
File left untouched.
|
|
21235
21633
|
`)
|
|
21236
21634
|
);
|
|
@@ -21241,30 +21639,30 @@ function runReverse(file, opts = {}) {
|
|
|
21241
21639
|
const what = lineage.exact ? `This map is mutation GENERATION ${lineage.generation}.` : `This map's "original" values look like Pretense synthetics (${lineage.syntheticCount} of ${plainMutations.length}).`;
|
|
21242
21640
|
if (!opts.allowPartial) {
|
|
21243
21641
|
console.error(
|
|
21244
|
-
|
|
21245
|
-
x NOT A RESTORE: ${what}`) +
|
|
21246
|
-
Its originals are themselves synthetics from an earlier mutation of this`) +
|
|
21247
|
-
file, so applying it would return the PREVIOUS MUTATED VERSION, not your`) +
|
|
21248
|
-
real secrets \u2014 while every integrity check passed, because the map is`) +
|
|
21249
|
-
self-consistent with the file. Reverse with the map from the FIRST`) +
|
|
21250
|
-
mutation round instead.`) +
|
|
21251
|
-
--allow-partial writes it anyway (useful only to walk a chain back one`) +
|
|
21252
|
-
generation at a time).`) +
|
|
21642
|
+
chalk17.red(`
|
|
21643
|
+
x NOT A RESTORE: ${what}`) + chalk17.dim(`
|
|
21644
|
+
Its originals are themselves synthetics from an earlier mutation of this`) + chalk17.dim(`
|
|
21645
|
+
file, so applying it would return the PREVIOUS MUTATED VERSION, not your`) + chalk17.dim(`
|
|
21646
|
+
real secrets \u2014 while every integrity check passed, because the map is`) + chalk17.dim(`
|
|
21647
|
+
self-consistent with the file. Reverse with the map from the FIRST`) + chalk17.dim(`
|
|
21648
|
+
mutation round instead.`) + chalk17.dim(`
|
|
21649
|
+
--allow-partial writes it anyway (useful only to walk a chain back one`) + chalk17.dim(`
|
|
21650
|
+
generation at a time).`) + chalk17.dim(`
|
|
21253
21651
|
File left untouched.
|
|
21254
21652
|
`)
|
|
21255
21653
|
);
|
|
21256
21654
|
return 1;
|
|
21257
21655
|
}
|
|
21258
21656
|
console.error(
|
|
21259
|
-
|
|
21260
|
-
! PARTIAL RESTORE (--allow-partial): ${what}`) +
|
|
21261
|
-
The output is the PREVIOUS MUTATED GENERATION of this file, NOT the`) +
|
|
21262
|
-
original. ${lineage.syntheticCount || "Some"} restored value(s) are still synthetic. Reverse again with`) +
|
|
21657
|
+
chalk17.yellow(`
|
|
21658
|
+
! PARTIAL RESTORE (--allow-partial): ${what}`) + chalk17.dim(`
|
|
21659
|
+
The output is the PREVIOUS MUTATED GENERATION of this file, NOT the`) + chalk17.dim(`
|
|
21660
|
+
original. ${lineage.syntheticCount || "Some"} restored value(s) are still synthetic. Reverse again with`) + chalk17.dim(`
|
|
21263
21661
|
the earlier round's map to reach the real secrets.
|
|
21264
21662
|
`)
|
|
21265
21663
|
);
|
|
21266
21664
|
}
|
|
21267
|
-
const mark = lineage.ok ?
|
|
21665
|
+
const mark = lineage.ok ? chalk17.green(" \u2713 Restored") : chalk17.yellow(" ! Partially restored");
|
|
21268
21666
|
const detail = `${appliedCount} of ${plainMutations.length} mapped tokens` + (lineage.ok ? "" : " \u2014 to an earlier MUTATED generation, not the original");
|
|
21269
21667
|
const preview = opts.stdout === true;
|
|
21270
21668
|
const inPlace = !preview && !opts.output;
|
|
@@ -21277,6 +21675,19 @@ function runReverse(file, opts = {}) {
|
|
|
21277
21675
|
} else {
|
|
21278
21676
|
process.stdout.write(restored);
|
|
21279
21677
|
}
|
|
21678
|
+
if (!preview && appliedCount > 0) {
|
|
21679
|
+
recordLocalAudit({
|
|
21680
|
+
action: "reversed",
|
|
21681
|
+
mutationsApplied: appliedCount,
|
|
21682
|
+
secretsBlocked: 0,
|
|
21683
|
+
piiRedacted: 0,
|
|
21684
|
+
originalSize: Buffer.byteLength(mutatedContent, "utf-8"),
|
|
21685
|
+
processedSize: Buffer.byteLength(restored, "utf-8"),
|
|
21686
|
+
durationMs: Date.now() - reverseStartMs,
|
|
21687
|
+
detectorKinds: [...new Set(mapFile.mutations.map((m) => m.type).filter((t) => typeof t === "string"))],
|
|
21688
|
+
file: absPath
|
|
21689
|
+
});
|
|
21690
|
+
}
|
|
21280
21691
|
if (opts.json) {
|
|
21281
21692
|
console.error(
|
|
21282
21693
|
JSON.stringify({
|
|
@@ -21294,7 +21705,7 @@ function runReverse(file, opts = {}) {
|
|
|
21294
21705
|
return 0;
|
|
21295
21706
|
}
|
|
21296
21707
|
function reverseCommand() {
|
|
21297
|
-
return new
|
|
21708
|
+
return new Command17("reverse").description(
|
|
21298
21709
|
"Restore a mutated file to its EXACT original bytes IN PLACE, auto-finding its reversal map (fails if the file was edited after mutation)"
|
|
21299
21710
|
).argument("<file>", "Mutated file to restore (must be byte-identical to the mutate output)").option(
|
|
21300
21711
|
"--map <path>",
|
|
@@ -21309,8 +21720,8 @@ function reverseCommand() {
|
|
|
21309
21720
|
|
|
21310
21721
|
// src/commands/run.ts
|
|
21311
21722
|
init_esm_shims();
|
|
21312
|
-
import { Command as
|
|
21313
|
-
import
|
|
21723
|
+
import { Command as Command18 } from "commander";
|
|
21724
|
+
import chalk18 from "chalk";
|
|
21314
21725
|
import { spawn } from "child_process";
|
|
21315
21726
|
import { basename as basename3 } from "path";
|
|
21316
21727
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -21384,20 +21795,23 @@ async function waitHealthy(port, deadlineMs, pollMs = 150) {
|
|
|
21384
21795
|
}
|
|
21385
21796
|
function spawnProxy(port) {
|
|
21386
21797
|
const cliEntry = process.argv[1] ?? fileURLToPath4(import.meta.url);
|
|
21387
|
-
|
|
21798
|
+
const startArgs = ["start", "--port", String(port), "--strict-port"];
|
|
21799
|
+
const isCompiledBinary = cliEntry.includes("$bunfs") || cliEntry.includes("~BUN");
|
|
21800
|
+
const spawnArgs = isCompiledBinary ? startArgs : [cliEntry, ...startArgs];
|
|
21801
|
+
return spawn(process.execPath, spawnArgs, {
|
|
21388
21802
|
stdio: ["ignore", "ignore", "pipe"],
|
|
21389
21803
|
env: process.env
|
|
21390
21804
|
});
|
|
21391
21805
|
}
|
|
21392
21806
|
function runCommand() {
|
|
21393
|
-
return new
|
|
21807
|
+
return new Command18("run").alias("with").description(
|
|
21394
21808
|
"Run an AI tool THROUGH the Pretense proxy with zero setup \u2014 auto-starts the proxy, sets the right base-URL env var, and stops the proxy it started when the tool exits"
|
|
21395
21809
|
).argument("<tool>", "The AI CLI to run (claude, cursor, gemini, \u2026)").argument("[args...]", "Arguments passed through to <tool>").option("--provider <name>", "Force the provider mapping: anthropic | openai | google").option("-p, --port <port>", "Preferred proxy port when starting one (default 9339)").option("--strict-port", "Fail instead of auto-advancing when starting a proxy on a busy port", false).passThroughOptions().allowUnknownOption().action(async (tool, args, opts) => {
|
|
21396
21810
|
let provider;
|
|
21397
21811
|
try {
|
|
21398
21812
|
provider = resolveProvider(tool, opts.provider);
|
|
21399
21813
|
} catch (err) {
|
|
21400
|
-
console.error(
|
|
21814
|
+
console.error(chalk18.red(`
|
|
21401
21815
|
x ${err.message}
|
|
21402
21816
|
`));
|
|
21403
21817
|
process.exit(1);
|
|
@@ -21408,13 +21822,13 @@ function runCommand() {
|
|
|
21408
21822
|
const rec = readProxyRecord();
|
|
21409
21823
|
if (rec && isProcessAlive(rec.pid) && await isProxyHealthy(rec.port)) {
|
|
21410
21824
|
port = rec.port;
|
|
21411
|
-
console.error(
|
|
21825
|
+
console.error(chalk18.dim(` \u21BA Reusing the Pretense proxy already running on port ${port}.`));
|
|
21412
21826
|
} else {
|
|
21413
21827
|
const preferred = opts.port ? parseInt(opts.port, 10) : 9339;
|
|
21414
21828
|
try {
|
|
21415
21829
|
port = await findFreePort(preferred);
|
|
21416
21830
|
} catch (err) {
|
|
21417
|
-
console.error(
|
|
21831
|
+
console.error(chalk18.red(`
|
|
21418
21832
|
x ${err.message}
|
|
21419
21833
|
`));
|
|
21420
21834
|
process.exit(1);
|
|
@@ -21428,14 +21842,14 @@ function runCommand() {
|
|
|
21428
21842
|
const ready = await waitHealthy(port, 15e3);
|
|
21429
21843
|
if (!ready) {
|
|
21430
21844
|
console.error(
|
|
21431
|
-
|
|
21432
|
-
x Pretense proxy did not become healthy on port ${port} in time.`) + (proxyStderr ?
|
|
21845
|
+
chalk18.red(`
|
|
21846
|
+
x Pretense proxy did not become healthy on port ${port} in time.`) + (proxyStderr ? chalk18.dim(`
|
|
21433
21847
|
${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
|
|
21434
21848
|
);
|
|
21435
21849
|
proxyChild.kill("SIGTERM");
|
|
21436
21850
|
process.exit(1);
|
|
21437
21851
|
}
|
|
21438
|
-
console.error(
|
|
21852
|
+
console.error(chalk18.green(` \u2713 Started Pretense proxy on port ${port}.`));
|
|
21439
21853
|
}
|
|
21440
21854
|
const stopStartedProxy = () => {
|
|
21441
21855
|
if (startedByUs && proxyChild && !proxyChild.killed) {
|
|
@@ -21444,7 +21858,7 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
|
|
|
21444
21858
|
};
|
|
21445
21859
|
const inject = injectedEnvFor(provider, port);
|
|
21446
21860
|
console.error(
|
|
21447
|
-
|
|
21861
|
+
chalk18.dim(
|
|
21448
21862
|
` \u2192 Routing ${basename3(tool)} through the proxy (${primaryEnvVar(provider)}=http://localhost:${port}).
|
|
21449
21863
|
`
|
|
21450
21864
|
)
|
|
@@ -21463,15 +21877,15 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
|
|
|
21463
21877
|
child.on("error", (err) => {
|
|
21464
21878
|
if (err.code === "ENOENT") {
|
|
21465
21879
|
console.error(
|
|
21466
|
-
|
|
21467
|
-
x "${tool}" is not on your PATH.`) +
|
|
21468
|
-
Install it (e.g. the ${basename3(tool)} CLI) and try again, or check the name.`) +
|
|
21469
|
-
You can still point a client at the proxy yourself:`) +
|
|
21880
|
+
chalk18.red(`
|
|
21881
|
+
x "${tool}" is not on your PATH.`) + chalk18.dim(`
|
|
21882
|
+
Install it (e.g. the ${basename3(tool)} CLI) and try again, or check the name.`) + chalk18.dim(`
|
|
21883
|
+
You can still point a client at the proxy yourself:`) + chalk18.dim(`
|
|
21470
21884
|
${primaryEnvVar(provider)}=http://localhost:${port}
|
|
21471
21885
|
`)
|
|
21472
21886
|
);
|
|
21473
21887
|
} else {
|
|
21474
|
-
console.error(
|
|
21888
|
+
console.error(chalk18.red(`
|
|
21475
21889
|
x Could not run "${tool}": ${err.message}
|
|
21476
21890
|
`));
|
|
21477
21891
|
}
|
|
@@ -21490,8 +21904,8 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
|
|
|
21490
21904
|
|
|
21491
21905
|
// src/commands/policy.ts
|
|
21492
21906
|
init_esm_shims();
|
|
21493
|
-
import { Command as
|
|
21494
|
-
import
|
|
21907
|
+
import { Command as Command19 } from "commander";
|
|
21908
|
+
import chalk19 from "chalk";
|
|
21495
21909
|
function listAction(opts) {
|
|
21496
21910
|
const ids = listPresetIds();
|
|
21497
21911
|
if (opts.json) {
|
|
@@ -21514,21 +21928,21 @@ function listAction(opts) {
|
|
|
21514
21928
|
);
|
|
21515
21929
|
return;
|
|
21516
21930
|
}
|
|
21517
|
-
console.log(
|
|
21931
|
+
console.log(chalk19.cyan("\n Pretense Compliance Frameworks\n"));
|
|
21518
21932
|
console.log(
|
|
21519
|
-
|
|
21933
|
+
chalk19.dim(
|
|
21520
21934
|
" " + "ID".padEnd(8) + "NAME".padEnd(14) + "TIER".padEnd(12) + "AUDIT".padEnd(8) + "DETECTORS"
|
|
21521
21935
|
)
|
|
21522
21936
|
);
|
|
21523
|
-
console.log(
|
|
21937
|
+
console.log(chalk19.dim(" " + "\u2500".repeat(64)));
|
|
21524
21938
|
for (const id of ids) {
|
|
21525
21939
|
const p = COMPLIANCE_PRESETS[id];
|
|
21526
21940
|
console.log(
|
|
21527
|
-
" " +
|
|
21941
|
+
" " + chalk19.bold(p.id.padEnd(8)) + p.name.padEnd(14) + chalk19.dim(p.minTier.padEnd(12)) + (p.auditRequired ? chalk19.green("yes".padEnd(8)) : chalk19.dim("no".padEnd(8))) + chalk19.dim(`${Object.keys(p.actionOverrides).length} overrides`)
|
|
21528
21942
|
);
|
|
21529
21943
|
}
|
|
21530
|
-
console.log(
|
|
21531
|
-
Run ${
|
|
21944
|
+
console.log(chalk19.dim(`
|
|
21945
|
+
Run ${chalk19.cyan("pretense policy show <id>")} for detector-level detail.
|
|
21532
21946
|
`));
|
|
21533
21947
|
}
|
|
21534
21948
|
function showAction(id, opts) {
|
|
@@ -21537,8 +21951,8 @@ function showAction(id, opts) {
|
|
|
21537
21951
|
preset = loadPreset(id);
|
|
21538
21952
|
} catch (err) {
|
|
21539
21953
|
console.error(
|
|
21540
|
-
|
|
21541
|
-
x ${err.message}`) +
|
|
21954
|
+
chalk19.red(`
|
|
21955
|
+
x ${err.message}`) + chalk19.dim(`
|
|
21542
21956
|
Valid frameworks: ${listPresetIds().join(", ")}
|
|
21543
21957
|
`)
|
|
21544
21958
|
);
|
|
@@ -21548,24 +21962,24 @@ function showAction(id, opts) {
|
|
|
21548
21962
|
console.log(JSON.stringify(preset, null, 2));
|
|
21549
21963
|
return;
|
|
21550
21964
|
}
|
|
21551
|
-
console.log(
|
|
21552
|
-
${preset.name} `) +
|
|
21553
|
-
console.log(
|
|
21965
|
+
console.log(chalk19.cyan(`
|
|
21966
|
+
${preset.name} `) + chalk19.dim(`(${preset.id})`));
|
|
21967
|
+
console.log(chalk19.dim(` ${preset.description}
|
|
21554
21968
|
`));
|
|
21555
|
-
console.log(` Minimum tier: ${
|
|
21556
|
-
console.log(` Audit required: ${preset.auditRequired ?
|
|
21557
|
-
console.log(` Detector sets: ${
|
|
21969
|
+
console.log(` Minimum tier: ${chalk19.bold(preset.minTier)}`);
|
|
21970
|
+
console.log(` Audit required: ${preset.auditRequired ? chalk19.green("yes") : chalk19.dim("no")}`);
|
|
21971
|
+
console.log(` Detector sets: ${chalk19.dim(preset.detectorSets.join(", "))}
|
|
21558
21972
|
`);
|
|
21559
21973
|
const overrides = Object.entries(preset.actionOverrides);
|
|
21560
|
-
console.log(
|
|
21974
|
+
console.log(chalk19.dim(` Detector actions (${overrides.length}):`));
|
|
21561
21975
|
for (const [detector, action] of overrides) {
|
|
21562
|
-
const color = action === "block" ?
|
|
21976
|
+
const color = action === "block" ? chalk19.red : action === "redact" ? chalk19.yellow : chalk19.dim;
|
|
21563
21977
|
console.log(` ${detector.padEnd(24)} ${color(action)}`);
|
|
21564
21978
|
}
|
|
21565
21979
|
console.log();
|
|
21566
21980
|
}
|
|
21567
21981
|
function policyCommand() {
|
|
21568
|
-
const cmd = new
|
|
21982
|
+
const cmd = new Command19("policy").description(
|
|
21569
21983
|
"List and inspect the built-in compliance presets (5: hipaa, gdpr, soc2, nist, pci)"
|
|
21570
21984
|
);
|
|
21571
21985
|
cmd.command("list").description("List the 5 built-in compliance presets").option("--json", "Output as JSON").action((opts) => listAction(opts));
|
|
@@ -21576,10 +21990,10 @@ function policyCommand() {
|
|
|
21576
21990
|
|
|
21577
21991
|
// src/commands/audit.ts
|
|
21578
21992
|
init_esm_shims();
|
|
21579
|
-
import { Command as
|
|
21580
|
-
import
|
|
21581
|
-
import { homedir as
|
|
21582
|
-
import { join as
|
|
21993
|
+
import { Command as Command20 } from "commander";
|
|
21994
|
+
import chalk20 from "chalk";
|
|
21995
|
+
import { homedir as homedir11 } from "os";
|
|
21996
|
+
import { join as join24 } from "path";
|
|
21583
21997
|
var VALID_FORMATS2 = ["table", "csv", "json", "jsonl"];
|
|
21584
21998
|
function toSafeAuditRow(e) {
|
|
21585
21999
|
return {
|
|
@@ -21650,18 +22064,18 @@ function resolveFormat2(opts) {
|
|
|
21650
22064
|
const raw2 = opts.format?.trim().toLowerCase();
|
|
21651
22065
|
if (raw2 !== void 0 && !VALID_FORMATS2.includes(raw2)) {
|
|
21652
22066
|
console.error(
|
|
21653
|
-
|
|
22067
|
+
chalk20.red(`
|
|
21654
22068
|
x Unknown --format: "${opts.format}"
|
|
21655
|
-
`) +
|
|
22069
|
+
`) + chalk20.dim(" Valid formats: table, csv, json (alias: jsonl).\n")
|
|
21656
22070
|
);
|
|
21657
22071
|
return null;
|
|
21658
22072
|
}
|
|
21659
22073
|
const fromFormat = raw2 === void 0 ? void 0 : raw2 === "jsonl" ? "json" : raw2;
|
|
21660
22074
|
if (opts.json && fromFormat !== void 0 && fromFormat !== "json") {
|
|
21661
22075
|
console.error(
|
|
21662
|
-
|
|
22076
|
+
chalk20.red(`
|
|
21663
22077
|
x --json and --format ${fromFormat} request different outputs.
|
|
21664
|
-
`) +
|
|
22078
|
+
`) + chalk20.dim(" Pick one.\n")
|
|
21665
22079
|
);
|
|
21666
22080
|
return null;
|
|
21667
22081
|
}
|
|
@@ -21669,17 +22083,17 @@ function resolveFormat2(opts) {
|
|
|
21669
22083
|
return opts.json ? "json" : "table";
|
|
21670
22084
|
}
|
|
21671
22085
|
function auditCommand() {
|
|
21672
|
-
return new
|
|
22086
|
+
return new Command20("audit").description(
|
|
21673
22087
|
"Show recent mutation/tokenization audit entries (leak-proof; JSONL with --json, RFC 4180 CSV with --format csv)"
|
|
21674
22088
|
).option("-l, --limit <n>", "Number of entries to show", "20").option("--json", "Output as JSONL (one JSON object per line, jq-pipeable)", false).option("--db <path>", "Path to the audit database (default: ~/.pretense/audit.db)").option(
|
|
21675
22089
|
"--framework <name>",
|
|
21676
|
-
"Only entries whose detectors map to this compliance framework (e.g. HIPAA, SOC2, PCI_DSS). Use --list-frameworks to see all
|
|
22090
|
+
"Only entries whose detectors map to this compliance framework (e.g. HIPAA, SOC2, PCI_DSS). Use --list-frameworks to see them all."
|
|
21677
22091
|
).option("--list-frameworks", "List the compliance frameworks that can be filtered on", false).option(
|
|
21678
22092
|
"--format <fmt>",
|
|
21679
22093
|
"Output format: table (default), csv (RFC 4180 compliance export), json (JSONL)"
|
|
21680
22094
|
).action((opts) => {
|
|
21681
22095
|
if (opts.listFrameworks) {
|
|
21682
|
-
for (const f of
|
|
22096
|
+
for (const f of POLICY_FRAMEWORKS) console.log(f);
|
|
21683
22097
|
return;
|
|
21684
22098
|
}
|
|
21685
22099
|
const format = resolveFormat2(opts);
|
|
@@ -21689,21 +22103,21 @@ function auditCommand() {
|
|
|
21689
22103
|
framework = resolveFrameworkName(opts.framework);
|
|
21690
22104
|
if (!framework) {
|
|
21691
22105
|
console.error(
|
|
21692
|
-
|
|
22106
|
+
chalk20.red(`
|
|
21693
22107
|
x Unknown framework: "${opts.framework}"
|
|
21694
|
-
`) +
|
|
22108
|
+
`) + chalk20.dim(" Run ") + chalk20.cyan("pretense audit --list-frameworks") + chalk20.dim(" to see them all.\n")
|
|
21695
22109
|
);
|
|
21696
22110
|
process.exit(1);
|
|
21697
22111
|
}
|
|
21698
22112
|
}
|
|
21699
22113
|
const limit = parseInt(opts.limit ?? "20", 10);
|
|
21700
22114
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
21701
|
-
console.error(
|
|
22115
|
+
console.error(chalk20.red(`
|
|
21702
22116
|
x Invalid --limit: "${opts.limit}". Provide a positive integer.
|
|
21703
22117
|
`));
|
|
21704
22118
|
process.exit(1);
|
|
21705
22119
|
}
|
|
21706
|
-
const dbPath = opts.db ??
|
|
22120
|
+
const dbPath = opts.db ?? join24(homedir11(), ".pretense", "audit.db");
|
|
21707
22121
|
let rows;
|
|
21708
22122
|
try {
|
|
21709
22123
|
const store = new AuditStore(dbPath);
|
|
@@ -21723,8 +22137,8 @@ function auditCommand() {
|
|
|
21723
22137
|
console.error("pretense: no audit data \u2014 the audit database has not been initialized yet.");
|
|
21724
22138
|
return;
|
|
21725
22139
|
}
|
|
21726
|
-
console.log(
|
|
21727
|
-
console.log(
|
|
22140
|
+
console.log(chalk20.dim("\n No audit data available. The audit database has not been initialized yet."));
|
|
22141
|
+
console.log(chalk20.dim(" Run " + chalk20.cyan("pretense start") + " and make some requests first.\n"));
|
|
21728
22142
|
return;
|
|
21729
22143
|
}
|
|
21730
22144
|
if (format === "csv") {
|
|
@@ -21736,23 +22150,23 @@ function auditCommand() {
|
|
|
21736
22150
|
return;
|
|
21737
22151
|
}
|
|
21738
22152
|
if (rows.length === 0) {
|
|
21739
|
-
console.log(
|
|
22153
|
+
console.log(chalk20.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
|
|
21740
22154
|
return;
|
|
21741
22155
|
}
|
|
21742
|
-
console.log(
|
|
22156
|
+
console.log(chalk20.cyan(`
|
|
21743
22157
|
Pretense Mutation Audit \u2014 last ${rows.length} entries
|
|
21744
22158
|
`));
|
|
21745
22159
|
console.log(
|
|
21746
|
-
|
|
22160
|
+
chalk20.dim(
|
|
21747
22161
|
" " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATED".padEnd(10) + "BLOCKED".padEnd(10) + "PII".padEnd(8) + "MS"
|
|
21748
22162
|
)
|
|
21749
22163
|
);
|
|
21750
|
-
console.log(
|
|
22164
|
+
console.log(chalk20.dim(" " + "\u2500".repeat(72)));
|
|
21751
22165
|
for (const row of rows) {
|
|
21752
22166
|
const time = new Date(row.timestamp).toTimeString().slice(0, 8);
|
|
21753
|
-
const actionColor = row.action === "blocked" ?
|
|
22167
|
+
const actionColor = row.action === "blocked" ? chalk20.red : row.action === "mutated" ? chalk20.green : row.action === "redacted" ? chalk20.yellow : row.action === "error" ? chalk20.red : chalk20.dim;
|
|
21754
22168
|
console.log(
|
|
21755
|
-
" " +
|
|
22169
|
+
" " + chalk20.dim(time.padEnd(10)) + actionColor(row.action.padEnd(12)) + chalk20.dim(row.provider.padEnd(14)) + chalk20.green(String(row.mutationsApplied).padEnd(10)) + (row.secretsBlocked > 0 ? chalk20.red(String(row.secretsBlocked).padEnd(10)) : chalk20.dim("0".padEnd(10))) + chalk20.dim(String(row.piiRedacted).padEnd(8)) + chalk20.dim(String(row.scanDurationMs).slice(0, 5))
|
|
21756
22170
|
);
|
|
21757
22171
|
}
|
|
21758
22172
|
console.log();
|
|
@@ -21761,10 +22175,10 @@ function auditCommand() {
|
|
|
21761
22175
|
|
|
21762
22176
|
// src/commands/credits.ts
|
|
21763
22177
|
init_esm_shims();
|
|
21764
|
-
import { Command as
|
|
21765
|
-
import
|
|
21766
|
-
import { join as
|
|
21767
|
-
import { homedir as
|
|
22178
|
+
import { Command as Command21 } from "commander";
|
|
22179
|
+
import chalk21 from "chalk";
|
|
22180
|
+
import { join as join25 } from "path";
|
|
22181
|
+
import { homedir as homedir12 } from "os";
|
|
21768
22182
|
function remaining(used, limit) {
|
|
21769
22183
|
if (limit === null) return null;
|
|
21770
22184
|
return Math.max(0, limit - used);
|
|
@@ -21773,16 +22187,16 @@ function fmt(n) {
|
|
|
21773
22187
|
return n === null ? "unlimited" : n.toLocaleString("en-US");
|
|
21774
22188
|
}
|
|
21775
22189
|
function bar(used, limit, width = 24) {
|
|
21776
|
-
if (limit === null) return
|
|
22190
|
+
if (limit === null) return chalk21.green("\u2588".repeat(width));
|
|
21777
22191
|
const pct = limit <= 0 ? 1 : Math.min(1, used / limit);
|
|
21778
22192
|
const filled = Math.min(width, Math.round(pct * width));
|
|
21779
22193
|
const s = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
21780
|
-
if (pct >= 1) return
|
|
21781
|
-
if (pct >= 0.8) return
|
|
21782
|
-
return
|
|
22194
|
+
if (pct >= 1) return chalk21.red(s);
|
|
22195
|
+
if (pct >= 0.8) return chalk21.yellow(s);
|
|
22196
|
+
return chalk21.green(s);
|
|
21783
22197
|
}
|
|
21784
22198
|
function creditsCommand() {
|
|
21785
|
-
return new
|
|
22199
|
+
return new Command21("credits").alias("tokens").description("Show remaining mutation budget for the current period").option("--db <path>", "Path to audit database", join25(homedir12(), ".pretense", "audit.db")).option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
|
|
21786
22200
|
const resolution = await resolvePlanTier({ offline: opts.offline });
|
|
21787
22201
|
const tier = resolution.tier;
|
|
21788
22202
|
const collected = collectUsage(opts.db);
|
|
@@ -21795,7 +22209,7 @@ function creditsCommand() {
|
|
|
21795
22209
|
periodEnd: null
|
|
21796
22210
|
};
|
|
21797
22211
|
if (!collected && !opts.json) {
|
|
21798
|
-
console.log(
|
|
22212
|
+
console.log(chalk21.dim("\n No usage recorded yet \u2014 showing your full allowance.\n"));
|
|
21799
22213
|
}
|
|
21800
22214
|
const mutationLimit = tier ? PLAN_CONFIGS[tier].mutationsPerPeriod : null;
|
|
21801
22215
|
const scanLimit = tier ? PLAN_CONFIGS[tier].scansPerPeriod : null;
|
|
@@ -21827,12 +22241,12 @@ function creditsCommand() {
|
|
|
21827
22241
|
);
|
|
21828
22242
|
return;
|
|
21829
22243
|
}
|
|
21830
|
-
console.log(
|
|
21831
|
-
console.log(
|
|
22244
|
+
console.log(chalk21.cyan("\n Pretense Credits"));
|
|
22245
|
+
console.log(chalk21.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetCadenceLabel()}
|
|
21832
22246
|
`));
|
|
21833
22247
|
if (!tier) {
|
|
21834
22248
|
for (const line of unverifiedNote(resolution.detail)) {
|
|
21835
|
-
console.log(
|
|
22249
|
+
console.log(chalk21.yellow(` ${line}`));
|
|
21836
22250
|
}
|
|
21837
22251
|
console.log();
|
|
21838
22252
|
console.log(" " + "Mutations used".padEnd(20) + m.mutationsApplied.toLocaleString("en-US"));
|
|
@@ -21841,18 +22255,18 @@ function creditsCommand() {
|
|
|
21841
22255
|
return;
|
|
21842
22256
|
}
|
|
21843
22257
|
console.log(
|
|
21844
|
-
" " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " +
|
|
22258
|
+
" " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " + chalk21.bold(fmt(mutationsLeft)) + chalk21.dim(` left of ${fmt(mutationLimit)}`)
|
|
21845
22259
|
);
|
|
21846
22260
|
console.log(
|
|
21847
|
-
" " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " +
|
|
22261
|
+
" " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " + chalk21.bold(fmt(scansLeft)) + chalk21.dim(` left of ${fmt(scanLimit)}`)
|
|
21848
22262
|
);
|
|
21849
22263
|
if (tier !== "enterprise" && mutationLimit !== null && mutationsLeft === 0) {
|
|
21850
22264
|
console.log(
|
|
21851
|
-
|
|
22265
|
+
chalk21.red("\n Mutation budget exhausted for this period.") + chalk21.dim(" Run ") + chalk21.cyan("pretense upgrade") + chalk21.dim(" to compare plans.")
|
|
21852
22266
|
);
|
|
21853
22267
|
} else if (tier !== "enterprise" && mutationLimit !== null && m.mutationsApplied >= mutationLimit * 0.8) {
|
|
21854
22268
|
console.log(
|
|
21855
|
-
|
|
22269
|
+
chalk21.yellow("\n Running low.") + chalk21.dim(" Run ") + chalk21.cyan("pretense upgrade") + chalk21.dim(" to compare plans.")
|
|
21856
22270
|
);
|
|
21857
22271
|
}
|
|
21858
22272
|
console.log();
|
|
@@ -21861,21 +22275,24 @@ function creditsCommand() {
|
|
|
21861
22275
|
|
|
21862
22276
|
// src/commands/upgrade.ts
|
|
21863
22277
|
init_esm_shims();
|
|
21864
|
-
import { Command as
|
|
21865
|
-
import
|
|
22278
|
+
import { Command as Command22 } from "commander";
|
|
22279
|
+
import chalk22 from "chalk";
|
|
21866
22280
|
var PRICING_URL = "https://pretense.ai/pricing";
|
|
21867
22281
|
var TIERS = ["free", "pro", "enterprise"];
|
|
21868
22282
|
function quota(n) {
|
|
21869
22283
|
return n === null ? "unlimited" : n.toLocaleString();
|
|
21870
22284
|
}
|
|
21871
22285
|
function seats(n) {
|
|
21872
|
-
return n === null ? "
|
|
22286
|
+
return n === null ? "up to 100" : String(n);
|
|
22287
|
+
}
|
|
22288
|
+
function comingSoonOrDash(enabled) {
|
|
22289
|
+
return enabled ? "coming soon" : "\u2014";
|
|
21873
22290
|
}
|
|
21874
22291
|
function yesNo(b) {
|
|
21875
22292
|
return b ? "yes" : "\u2014";
|
|
21876
22293
|
}
|
|
21877
22294
|
function upgradeCommand() {
|
|
21878
|
-
return new
|
|
22295
|
+
return new Command22("upgrade").description("Compare Pretense plans and upgrade").option("--json", "Output as JSON", false).option("--offline", "Do not contact the plan service to resolve the tier", false).action(async (opts) => {
|
|
21879
22296
|
const resolution = await resolvePlanTier({ offline: opts.offline });
|
|
21880
22297
|
const current = resolution.tier;
|
|
21881
22298
|
if (opts.json) {
|
|
@@ -21903,40 +22320,41 @@ function upgradeCommand() {
|
|
|
21903
22320
|
);
|
|
21904
22321
|
return;
|
|
21905
22322
|
}
|
|
21906
|
-
console.log(
|
|
22323
|
+
console.log(chalk22.cyan("\n Pretense Plans\n"));
|
|
21907
22324
|
const col = 16;
|
|
21908
22325
|
const header = " " + "".padEnd(18) + TIERS.map((t) => tierName(t).padEnd(col)).join("");
|
|
21909
|
-
console.log(
|
|
21910
|
-
console.log(" " +
|
|
22326
|
+
console.log(chalk22.bold(header));
|
|
22327
|
+
console.log(" " + chalk22.dim("\u2500".repeat(18 + col * TIERS.length)));
|
|
21911
22328
|
const rows = [
|
|
21912
22329
|
["Price", (t) => priceLabel(t)],
|
|
21913
22330
|
["Mutations/7d", (t) => quota(PLAN_CONFIGS[t].mutationsPerPeriod)],
|
|
21914
22331
|
["Requests/7d", (t) => quota(PLAN_CONFIGS[t].scansPerPeriod)],
|
|
21915
22332
|
["Seats", (t) => seats(PLAN_CONFIGS[t].maxSeats)],
|
|
21916
|
-
["SSO", (t) =>
|
|
22333
|
+
["SSO", (t) => comingSoonOrDash(PLAN_CONFIGS[t].ssoEnabled)],
|
|
22334
|
+
// On-prem IS truthful — Docker + Helm ship — so it stays yes/—.
|
|
21917
22335
|
["On-prem", (t) => yesNo(PLAN_CONFIGS[t].onPremEnabled)]
|
|
21918
22336
|
];
|
|
21919
22337
|
for (const [label, cell] of rows) {
|
|
21920
22338
|
console.log(" " + label.padEnd(18) + TIERS.map((t) => cell(t).padEnd(col)).join(""));
|
|
21921
22339
|
}
|
|
21922
|
-
console.log(" " +
|
|
22340
|
+
console.log(" " + chalk22.dim("\u2500".repeat(18 + col * TIERS.length)));
|
|
21923
22341
|
if (current) {
|
|
21924
|
-
console.log(
|
|
22342
|
+
console.log(chalk22.dim(`
|
|
21925
22343
|
Current plan: ${tierName(current)}`));
|
|
21926
22344
|
if (current === "enterprise") {
|
|
21927
|
-
console.log(
|
|
22345
|
+
console.log(chalk22.green(" You are on the highest tier \u2014 nothing to upgrade.\n"));
|
|
21928
22346
|
return;
|
|
21929
22347
|
}
|
|
21930
22348
|
} else {
|
|
21931
22349
|
console.log(
|
|
21932
|
-
|
|
22350
|
+
chalk22.yellow(
|
|
21933
22351
|
`
|
|
21934
22352
|
Could not confirm your current plan: ${resolution.detail ?? "unknown reason"}.`
|
|
21935
22353
|
)
|
|
21936
22354
|
);
|
|
21937
22355
|
}
|
|
21938
22356
|
const from = current ?? "unknown";
|
|
21939
|
-
console.log(
|
|
22357
|
+
console.log(chalk22.bold(`
|
|
21940
22358
|
\u2192 ${PRICING_URL}?ref=cli_upgrade&from=${from}
|
|
21941
22359
|
`));
|
|
21942
22360
|
});
|
|
@@ -21969,35 +22387,36 @@ function findClosestCommand(input, knownCommands) {
|
|
|
21969
22387
|
}
|
|
21970
22388
|
return bestDist <= 3 ? best : null;
|
|
21971
22389
|
}
|
|
21972
|
-
var program = new
|
|
21973
|
-
program.name("pretense").enablePositionalOptions().description(
|
|
21974
|
-
${
|
|
21975
|
-
${
|
|
21976
|
-
${
|
|
21977
|
-
${
|
|
22390
|
+
var program = new Command23();
|
|
22391
|
+
program.name("pretense").enablePositionalOptions().description(chalk23.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version(PRETENSE_VERSION, "-v, --version").addHelpText("after", `
|
|
22392
|
+
${chalk23.bold("Quick start:")}
|
|
22393
|
+
${chalk23.cyan("pretense run claude")} ${chalk23.dim('"build X"')} Run an AI tool through the proxy (zero setup)
|
|
22394
|
+
${chalk23.cyan("pretense mutate")} ${chalk23.dim("<file>")} Mutate secrets in place (reverse with the same path)
|
|
22395
|
+
${chalk23.cyan("pretense scan")} ${chalk23.dim("<file|dir>")} Scan for secrets and PII
|
|
21978
22396
|
|
|
21979
|
-
${
|
|
21980
|
-
${
|
|
21981
|
-
${
|
|
21982
|
-
${
|
|
21983
|
-
${
|
|
21984
|
-
${
|
|
21985
|
-
${
|
|
21986
|
-
${
|
|
21987
|
-
${
|
|
21988
|
-
${
|
|
21989
|
-
${
|
|
21990
|
-
${
|
|
21991
|
-
${
|
|
21992
|
-
${
|
|
21993
|
-
${
|
|
21994
|
-
${
|
|
21995
|
-
${
|
|
21996
|
-
${
|
|
21997
|
-
${
|
|
21998
|
-
${
|
|
22397
|
+
${chalk23.dim("All commands:")}
|
|
22398
|
+
${chalk23.cyan("pretense init")} Scan codebase and build protection profile
|
|
22399
|
+
${chalk23.cyan("pretense run <tool>")} Run an AI tool through the proxy, no manual export
|
|
22400
|
+
${chalk23.cyan("pretense start")} Start proxy on localhost:9339
|
|
22401
|
+
${chalk23.cyan("pretense stop")} Stop the running proxy
|
|
22402
|
+
${chalk23.cyan("pretense scan src/api.ts")} Scan file for secrets
|
|
22403
|
+
${chalk23.cyan("pretense scan ci")} CI-optimized scan (exit 0=clean, 2=findings)
|
|
22404
|
+
${chalk23.cyan("pretense status")} Show protection status
|
|
22405
|
+
${chalk23.cyan("pretense doctor")} Diagnose why the proxy isn't catching traffic
|
|
22406
|
+
${chalk23.cyan("pretense review")} Preview mutations that would be applied
|
|
22407
|
+
${chalk23.cyan("pretense review --json")} Output review as JSON
|
|
22408
|
+
${chalk23.cyan("pretense mutate <file>")} Mutate secrets in place + auto-write reversal map
|
|
22409
|
+
${chalk23.cyan("pretense reverse <file>")} Restore a mutated file (auto-finds its map)
|
|
22410
|
+
${chalk23.cyan("pretense policy list")} List the 5 compliance presets
|
|
22411
|
+
${chalk23.cyan("pretense audit")} Show mutation audit log
|
|
22412
|
+
${chalk23.cyan("pretense usage")} Plan usage vs limits
|
|
22413
|
+
${chalk23.cyan("pretense credits")} Remaining mutation budget (alias: tokens)
|
|
22414
|
+
${chalk23.cyan("pretense upgrade")} Compare plans / upgrade
|
|
22415
|
+
${chalk23.cyan("pretense auth login")} Authenticate with Pretense
|
|
22416
|
+
${chalk23.cyan("pretense install")} Install git hooks
|
|
22417
|
+
${chalk23.cyan("pretense logs --limit 20")} Show recent audit entries
|
|
21999
22418
|
|
|
22000
|
-
${
|
|
22419
|
+
${chalk23.dim("Run")} ${chalk23.cyan("pretense <command> --help")} ${chalk23.dim("for command-specific help.")}
|
|
22001
22420
|
`);
|
|
22002
22421
|
var KNOWN_COMMANDS = [
|
|
22003
22422
|
"init",
|
|
@@ -22025,7 +22444,8 @@ var KNOWN_COMMANDS = [
|
|
|
22025
22444
|
"logout",
|
|
22026
22445
|
"credits",
|
|
22027
22446
|
"tokens",
|
|
22028
|
-
"upgrade"
|
|
22447
|
+
"upgrade",
|
|
22448
|
+
"doctor"
|
|
22029
22449
|
];
|
|
22030
22450
|
program.command("version").description("Print Pretense CLI version").action(() => {
|
|
22031
22451
|
console.log(`@pretense/cli v${program.version()}`);
|
|
@@ -22058,16 +22478,17 @@ program.addCommand(policyCommand());
|
|
|
22058
22478
|
program.addCommand(auditCommand());
|
|
22059
22479
|
program.addCommand(creditsCommand());
|
|
22060
22480
|
program.addCommand(upgradeCommand());
|
|
22481
|
+
program.addCommand(doctorCommand());
|
|
22061
22482
|
program.on("command:*", (operands) => {
|
|
22062
22483
|
const unknown = operands[0] ?? "";
|
|
22063
22484
|
const suggestion = findClosestCommand(unknown, KNOWN_COMMANDS);
|
|
22064
|
-
console.error(
|
|
22485
|
+
console.error(chalk23.red(`
|
|
22065
22486
|
x Unknown command: "${unknown}"`));
|
|
22066
22487
|
if (suggestion) {
|
|
22067
|
-
console.error(
|
|
22488
|
+
console.error(chalk23.yellow(` Did you mean: ${chalk23.bold(`pretense ${suggestion}`)}`));
|
|
22068
22489
|
}
|
|
22069
|
-
console.error(
|
|
22070
|
-
Run ${
|
|
22490
|
+
console.error(chalk23.dim(`
|
|
22491
|
+
Run ${chalk23.cyan("pretense --help")} to see all available commands.
|
|
22071
22492
|
`));
|
|
22072
22493
|
process.exit(1);
|
|
22073
22494
|
});
|