@pretense/cli 0.6.15 → 0.6.17
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-EFDPBFXJ.js} +1 -1
- package/dist/index.js +1510 -1044
- package/dist/postinstall.js +1 -1
- package/package.json +5 -5
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-EFDPBFXJ.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();
|
|
@@ -3892,10 +3892,10 @@ var Minipass = class extends EventEmitter {
|
|
|
3892
3892
|
* Return a void Promise that resolves once the stream ends.
|
|
3893
3893
|
*/
|
|
3894
3894
|
async promise() {
|
|
3895
|
-
return new Promise((
|
|
3895
|
+
return new Promise((resolve8, reject) => {
|
|
3896
3896
|
this.on(DESTROYED, () => reject(new Error("stream destroyed")));
|
|
3897
3897
|
this.on("error", (er) => reject(er));
|
|
3898
|
-
this.on("end", () =>
|
|
3898
|
+
this.on("end", () => resolve8());
|
|
3899
3899
|
});
|
|
3900
3900
|
}
|
|
3901
3901
|
/**
|
|
@@ -3919,7 +3919,7 @@ var Minipass = class extends EventEmitter {
|
|
|
3919
3919
|
return Promise.resolve({ done: false, value: res });
|
|
3920
3920
|
if (this[EOF])
|
|
3921
3921
|
return stop();
|
|
3922
|
-
let
|
|
3922
|
+
let resolve8;
|
|
3923
3923
|
let reject;
|
|
3924
3924
|
const onerr = (er) => {
|
|
3925
3925
|
this.off("data", ondata);
|
|
@@ -3933,19 +3933,19 @@ var Minipass = class extends EventEmitter {
|
|
|
3933
3933
|
this.off("end", onend);
|
|
3934
3934
|
this.off(DESTROYED, ondestroy);
|
|
3935
3935
|
this.pause();
|
|
3936
|
-
|
|
3936
|
+
resolve8({ value, done: !!this[EOF] });
|
|
3937
3937
|
};
|
|
3938
3938
|
const onend = () => {
|
|
3939
3939
|
this.off("error", onerr);
|
|
3940
3940
|
this.off("data", ondata);
|
|
3941
3941
|
this.off(DESTROYED, ondestroy);
|
|
3942
3942
|
stop();
|
|
3943
|
-
|
|
3943
|
+
resolve8({ done: true, value: void 0 });
|
|
3944
3944
|
};
|
|
3945
3945
|
const ondestroy = () => onerr(new Error("stream destroyed"));
|
|
3946
3946
|
return new Promise((res2, rej) => {
|
|
3947
3947
|
reject = rej;
|
|
3948
|
-
|
|
3948
|
+
resolve8 = res2;
|
|
3949
3949
|
this.once(DESTROYED, ondestroy);
|
|
3950
3950
|
this.once("error", onerr);
|
|
3951
3951
|
this.once("end", onend);
|
|
@@ -4917,9 +4917,9 @@ var PathBase = class {
|
|
|
4917
4917
|
if (this.#asyncReaddirInFlight) {
|
|
4918
4918
|
await this.#asyncReaddirInFlight;
|
|
4919
4919
|
} else {
|
|
4920
|
-
let
|
|
4920
|
+
let resolve8 = () => {
|
|
4921
4921
|
};
|
|
4922
|
-
this.#asyncReaddirInFlight = new Promise((res) =>
|
|
4922
|
+
this.#asyncReaddirInFlight = new Promise((res) => resolve8 = res);
|
|
4923
4923
|
try {
|
|
4924
4924
|
for (const e of await this.#fs.promises.readdir(fullpath, {
|
|
4925
4925
|
withFileTypes: true
|
|
@@ -4932,7 +4932,7 @@ var PathBase = class {
|
|
|
4932
4932
|
children.provisional = 0;
|
|
4933
4933
|
}
|
|
4934
4934
|
this.#asyncReaddirInFlight = void 0;
|
|
4935
|
-
|
|
4935
|
+
resolve8();
|
|
4936
4936
|
}
|
|
4937
4937
|
return children.slice(0, children.provisional);
|
|
4938
4938
|
}
|
|
@@ -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
|
|
|
@@ -10400,7 +10413,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
|
|
|
10400
10413
|
});
|
|
10401
10414
|
if (!chunk) {
|
|
10402
10415
|
if (i === 1) {
|
|
10403
|
-
await new Promise((
|
|
10416
|
+
await new Promise((resolve8) => setTimeout(resolve8));
|
|
10404
10417
|
maxReadCount = 3;
|
|
10405
10418
|
continue;
|
|
10406
10419
|
}
|
|
@@ -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);
|
|
@@ -14374,7 +14408,7 @@ import { createHmac, hkdfSync, randomBytes as randomBytes2 } from "crypto";
|
|
|
14374
14408
|
import { createHash, randomBytes } from "crypto";
|
|
14375
14409
|
import { readFileSync as readFileSync4, existsSync as existsSync22 } from "fs";
|
|
14376
14410
|
import { homedir as homedir2 } from "os";
|
|
14377
|
-
import { join as join22, dirname, parse } from "path";
|
|
14411
|
+
import { join as join22, dirname, parse, resolve } from "path";
|
|
14378
14412
|
import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
14379
14413
|
import { homedir } from "os";
|
|
14380
14414
|
import { join as join4 } from "path";
|
|
@@ -14447,6 +14481,7 @@ function tighten(path2, exact, kind) {
|
|
|
14447
14481
|
);
|
|
14448
14482
|
}
|
|
14449
14483
|
}
|
|
14484
|
+
var PROJECT_ROOT_ENV = "PRETENSE_PROJECT_ROOT";
|
|
14450
14485
|
var KEYS_DIR_NAME = join22(".pretense", "keys");
|
|
14451
14486
|
function keysDir() {
|
|
14452
14487
|
return join22(homedir2(), KEYS_DIR_NAME);
|
|
@@ -14469,7 +14504,25 @@ var PROJECT_MARKERS = [
|
|
|
14469
14504
|
".hg",
|
|
14470
14505
|
".svn"
|
|
14471
14506
|
];
|
|
14507
|
+
function markersAt(dir, home) {
|
|
14508
|
+
return dir === home ? PROJECT_MARKERS.filter((m) => m !== ".pretense") : PROJECT_MARKERS;
|
|
14509
|
+
}
|
|
14472
14510
|
function findProjectRoot(filePath) {
|
|
14511
|
+
let dir = dirname(filePath);
|
|
14512
|
+
const { root } = parse(dir);
|
|
14513
|
+
const startDir = dir;
|
|
14514
|
+
const home = homedir2();
|
|
14515
|
+
while (true) {
|
|
14516
|
+
if (markersAt(dir, home).some((m) => existsSync22(join22(dir, m)))) {
|
|
14517
|
+
return dir;
|
|
14518
|
+
}
|
|
14519
|
+
if (dir === root) return startDir;
|
|
14520
|
+
const parent = dirname(dir);
|
|
14521
|
+
if (parent === dir) return startDir;
|
|
14522
|
+
dir = parent;
|
|
14523
|
+
}
|
|
14524
|
+
}
|
|
14525
|
+
function legacyFindProjectRoot(filePath) {
|
|
14473
14526
|
let dir = dirname(filePath);
|
|
14474
14527
|
const { root } = parse(dir);
|
|
14475
14528
|
const startDir = dir;
|
|
@@ -14483,6 +14536,21 @@ function findProjectRoot(filePath) {
|
|
|
14483
14536
|
dir = parent;
|
|
14484
14537
|
}
|
|
14485
14538
|
}
|
|
14539
|
+
function resolveProjectRoot(filePath) {
|
|
14540
|
+
const override2 = process.env[PROJECT_ROOT_ENV];
|
|
14541
|
+
if (override2 && override2.trim()) return resolve(override2.trim());
|
|
14542
|
+
return findProjectRoot(filePath);
|
|
14543
|
+
}
|
|
14544
|
+
function candidateProjectRoots(filePath) {
|
|
14545
|
+
const out = [];
|
|
14546
|
+
const override2 = process.env[PROJECT_ROOT_ENV];
|
|
14547
|
+
if (override2 && override2.trim()) out.push(resolve(override2.trim()));
|
|
14548
|
+
out.push(findProjectRoot(filePath));
|
|
14549
|
+
out.push(dirname(filePath));
|
|
14550
|
+
out.push(legacyFindProjectRoot(filePath));
|
|
14551
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14552
|
+
return out.filter((r) => seen.has(r) ? false : (seen.add(r), true));
|
|
14553
|
+
}
|
|
14486
14554
|
function findProjectRootForDir(dir) {
|
|
14487
14555
|
return findProjectRoot(join22(dir, "__pretense_probe__"));
|
|
14488
14556
|
}
|
|
@@ -16118,6 +16186,7 @@ var AuditStore = class {
|
|
|
16118
16186
|
blocked: 0,
|
|
16119
16187
|
mutated: 0,
|
|
16120
16188
|
redacted: 0,
|
|
16189
|
+
reversed: 0,
|
|
16121
16190
|
error: 0
|
|
16122
16191
|
};
|
|
16123
16192
|
for (const row of actionRows) {
|
|
@@ -16300,7 +16369,7 @@ import { join as join5 } from "path";
|
|
|
16300
16369
|
import { parse as parseYaml } from "yaml";
|
|
16301
16370
|
import { readFileSync as readFileSync23 } from "fs";
|
|
16302
16371
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
16303
|
-
import { dirname as dirname3, join as join23, resolve } from "path";
|
|
16372
|
+
import { dirname as dirname3, join as join23, resolve as resolve2 } from "path";
|
|
16304
16373
|
import https from "https";
|
|
16305
16374
|
|
|
16306
16375
|
// ../packages/tls/dist/index.js
|
|
@@ -16436,7 +16505,7 @@ function versionFromCliPackageJson() {
|
|
|
16436
16505
|
}
|
|
16437
16506
|
} catch {
|
|
16438
16507
|
}
|
|
16439
|
-
const parent =
|
|
16508
|
+
const parent = resolve2(dir, "..");
|
|
16440
16509
|
if (parent === dir) break;
|
|
16441
16510
|
dir = parent;
|
|
16442
16511
|
}
|
|
@@ -16445,11 +16514,11 @@ function versionFromCliPackageJson() {
|
|
|
16445
16514
|
return void 0;
|
|
16446
16515
|
}
|
|
16447
16516
|
function productVersion() {
|
|
16448
|
-
return override ?? (true ? "0.6.
|
|
16517
|
+
return override ?? (true ? "0.6.17" : void 0) ?? versionFromCliPackageJson() ?? "0.0.0-unknown";
|
|
16449
16518
|
}
|
|
16450
16519
|
var UNKNOWN_COMMIT = "unknown";
|
|
16451
16520
|
function bakedCommitSha() {
|
|
16452
|
-
return "
|
|
16521
|
+
return "6f467e565f6f035a63fe420acef1019ddf2455f0".length > 0 ? "6f467e565f6f035a63fe420acef1019ddf2455f0" : UNKNOWN_COMMIT;
|
|
16453
16522
|
}
|
|
16454
16523
|
var FEATURE_TIERS = {
|
|
16455
16524
|
compliance_export: "pro",
|
|
@@ -16571,7 +16640,10 @@ var PlanEnforcer = class {
|
|
|
16571
16640
|
upgradeMessage,
|
|
16572
16641
|
upgradeTier,
|
|
16573
16642
|
upgradeUrl,
|
|
16574
|
-
resetDate
|
|
16643
|
+
resetDate,
|
|
16644
|
+
limitMetric: worstMetric,
|
|
16645
|
+
limitCurrent: worstCurrent,
|
|
16646
|
+
limitValue: worstLimit
|
|
16575
16647
|
};
|
|
16576
16648
|
}
|
|
16577
16649
|
if (overallPercent >= 90) {
|
|
@@ -17514,20 +17586,26 @@ function createProxy(opts = {}) {
|
|
|
17514
17586
|
if (_enforcement.upgradeUrl) c.header("X-Pretense-Upgrade-Url", _enforcement.upgradeUrl);
|
|
17515
17587
|
}
|
|
17516
17588
|
if (!_enforcement.allowed) {
|
|
17517
|
-
const _planConfig = PLAN_CONFIGS[_effectiveTier];
|
|
17518
|
-
const _mutLimit = _planConfig.mutationsPerPeriod ?? 0;
|
|
17519
|
-
const _mutCurrent = _periodStats.totalMutationsApplied;
|
|
17520
17589
|
const _upgradeTier = _enforcement.upgradeTier ?? "pro";
|
|
17590
|
+
const _metric = _enforcement.limitMetric ?? "mutations";
|
|
17591
|
+
const _mutCurrent = _enforcement.limitCurrent ?? _periodStats.totalMutationsApplied;
|
|
17592
|
+
const _mutLimit = _enforcement.limitValue ?? (PLAN_CONFIGS[_effectiveTier].mutationsPerPeriod ?? 0);
|
|
17593
|
+
const _metricLabel = {
|
|
17594
|
+
mutations: "Mutation",
|
|
17595
|
+
scans: "Request",
|
|
17596
|
+
bytes: "Data",
|
|
17597
|
+
seats: "Seat"
|
|
17598
|
+
};
|
|
17599
|
+
const _label = _metricLabel[_metric] ?? "Usage";
|
|
17521
17600
|
return c.json({
|
|
17522
17601
|
error: {
|
|
17523
17602
|
type: "plan_limit_exceeded",
|
|
17524
|
-
code:
|
|
17525
|
-
// Human-readable error matching the spec format
|
|
17603
|
+
code: `${_metric.toUpperCase()}_LIMIT_EXCEEDED`,
|
|
17526
17604
|
// 100,000 = PLAN_CONFIGS.pro.mutationsPerPeriod; $29/seat =
|
|
17527
17605
|
// PLAN_CONFIGS.pro.pricePerSeatCents (monthly rate). Enterprise
|
|
17528
17606
|
// prints no price and no self-serve `pretense upgrade` path: it
|
|
17529
17607
|
// is contact-sales only. DO NOT restore "$99/seat".
|
|
17530
|
-
message: `\u2717
|
|
17608
|
+
message: `\u2717 ${_label} limit reached for this 7-day period (${_mutCurrent.toLocaleString()}/${_mutLimit.toLocaleString()})
|
|
17531
17609
|
|
|
17532
17610
|
${_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
17611
|
upgrade: {
|
|
@@ -17540,7 +17618,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17540
17618
|
current: _mutCurrent,
|
|
17541
17619
|
limit: _mutLimit,
|
|
17542
17620
|
percent: _enforcement.usagePercent,
|
|
17543
|
-
metric:
|
|
17621
|
+
metric: _metric,
|
|
17544
17622
|
resetDate: _enforcement.resetDate
|
|
17545
17623
|
}
|
|
17546
17624
|
}
|
|
@@ -17681,6 +17759,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17681
17759
|
return replaceCodeBlocks(text, blocks, mutatedBlocks);
|
|
17682
17760
|
});
|
|
17683
17761
|
const mutDurationMs = Math.round((performance.now() - mutT0) * 100) / 100;
|
|
17762
|
+
const mutationsApplied = tokensMutated + secretsTokenized + piiTokenized;
|
|
17684
17763
|
const totalChars = fullText.length;
|
|
17685
17764
|
const identifierExposureRate = totalChars > 0 ? Math.min(1, tokensMutated / Math.max(1, totalChars / 10)) : 0;
|
|
17686
17765
|
const riskCtx = {
|
|
@@ -17709,7 +17788,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17709
17788
|
processedSize: 0,
|
|
17710
17789
|
secretsBlocked: secretsTokenized,
|
|
17711
17790
|
piiRedacted: piiTokenized,
|
|
17712
|
-
mutationsApplied
|
|
17791
|
+
mutationsApplied,
|
|
17713
17792
|
scanDurationMs,
|
|
17714
17793
|
mutationDurationMs: mutDurationMs,
|
|
17715
17794
|
action: "blocked",
|
|
@@ -17791,7 +17870,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17791
17870
|
processedSize,
|
|
17792
17871
|
secretsBlocked: secretsTokenized,
|
|
17793
17872
|
piiRedacted: piiTokenized,
|
|
17794
|
-
mutationsApplied
|
|
17873
|
+
mutationsApplied,
|
|
17795
17874
|
scanDurationMs,
|
|
17796
17875
|
mutationDurationMs: mutDurationMs,
|
|
17797
17876
|
action: "error",
|
|
@@ -17828,7 +17907,7 @@ ${_upgradeTier === "enterprise" ? "Enterprise offers unlimited mutations \u2014
|
|
|
17828
17907
|
processedSize,
|
|
17829
17908
|
secretsBlocked: secretsTokenized,
|
|
17830
17909
|
piiRedacted: piiTokenized,
|
|
17831
|
-
mutationsApplied
|
|
17910
|
+
mutationsApplied,
|
|
17832
17911
|
scanDurationMs,
|
|
17833
17912
|
mutationDurationMs: mutDurationMs,
|
|
17834
17913
|
action,
|
|
@@ -17904,10 +17983,16 @@ function renderClientEnvLines(port, protocol) {
|
|
|
17904
17983
|
" Easiest \u2014 let Pretense wire the env var and run the tool for you:",
|
|
17905
17984
|
' pretense run claude "build me X" # or: cursor, gemini, any tool',
|
|
17906
17985
|
"",
|
|
17907
|
-
|
|
17908
|
-
`
|
|
17909
|
-
|
|
17910
|
-
`
|
|
17986
|
+
// The manual lines are SAME-SHELL (env var and tool on one command) on
|
|
17987
|
+
// purpose: a user who runs `export …BASE_URL=…` here and then `claude` in
|
|
17988
|
+
// a NEW terminal is unprotected — env vars are per-shell. The warning
|
|
17989
|
+
// below states that, and points back at `pretense run` as the fix.
|
|
17990
|
+
" Or point your AI client at Pretense yourself (SAME terminal as the tool):",
|
|
17991
|
+
` ANTHROPIC_BASE_URL=${base} claude`,
|
|
17992
|
+
` OPENAI_BASE_URL=${base}/v1 cursor`,
|
|
17993
|
+
` GOOGLE_GEMINI_BASE_URL=${base} gemini`,
|
|
17994
|
+
" \u26A0 Set this in the SAME terminal as your AI tool \u2014 a new terminal is NOT",
|
|
17995
|
+
" protected. Simplest: use `pretense run` and skip the export entirely."
|
|
17911
17996
|
].join("\n") + "\n";
|
|
17912
17997
|
}
|
|
17913
17998
|
function printBanner(port, protocol, tls) {
|
|
@@ -17938,14 +18023,14 @@ if (isMain) {
|
|
|
17938
18023
|
init_esm_shims();
|
|
17939
18024
|
import net from "net";
|
|
17940
18025
|
function isPortFree(port) {
|
|
17941
|
-
return new Promise((
|
|
18026
|
+
return new Promise((resolve8) => {
|
|
17942
18027
|
const srv = net.createServer();
|
|
17943
18028
|
srv.once("error", () => {
|
|
17944
|
-
srv.close(() =>
|
|
17945
|
-
|
|
18029
|
+
srv.close(() => resolve8(false));
|
|
18030
|
+
resolve8(false);
|
|
17946
18031
|
});
|
|
17947
18032
|
srv.once("listening", () => {
|
|
17948
|
-
srv.close(() =>
|
|
18033
|
+
srv.close(() => resolve8(true));
|
|
17949
18034
|
});
|
|
17950
18035
|
srv.listen(port);
|
|
17951
18036
|
});
|
|
@@ -18051,6 +18136,16 @@ async function stopProxy(path2 = pidFilePath(), graceMs = 2e3, pollMs = 50) {
|
|
|
18051
18136
|
}
|
|
18052
18137
|
|
|
18053
18138
|
// src/commands/start.ts
|
|
18139
|
+
function parsePort(raw2) {
|
|
18140
|
+
if (!/^\d+$/.test(raw2.trim())) {
|
|
18141
|
+
throw new InvalidArgumentError(`"${raw2}" is not a number. Use a port between 1 and 65535.`);
|
|
18142
|
+
}
|
|
18143
|
+
const n = Number(raw2);
|
|
18144
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
18145
|
+
throw new InvalidArgumentError(`${raw2} is out of range. Use a port between 1 and 65535.`);
|
|
18146
|
+
}
|
|
18147
|
+
return String(n);
|
|
18148
|
+
}
|
|
18054
18149
|
function pidListeningOn(port) {
|
|
18055
18150
|
try {
|
|
18056
18151
|
const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, {
|
|
@@ -18084,7 +18179,7 @@ function explainAddrInUse(port) {
|
|
|
18084
18179
|
);
|
|
18085
18180
|
}
|
|
18086
18181
|
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) => {
|
|
18182
|
+
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
18183
|
const requested = parseInt(opts.port, 10);
|
|
18089
18184
|
const existing = readProxyRecord();
|
|
18090
18185
|
if (existing) {
|
|
@@ -18092,7 +18187,10 @@ function startCommand() {
|
|
|
18092
18187
|
if (existing.port === requested) {
|
|
18093
18188
|
console.error(
|
|
18094
18189
|
chalk2.red(`
|
|
18095
|
-
x Pretense proxy is already running (pid ${existing.pid}, port ${existing.port}).`) +
|
|
18190
|
+
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
|
|
18191
|
+
// tool through it, and `pretense run` reuses this exact proxy
|
|
18192
|
+
// (no second export, no second terminal). Lead with that.
|
|
18193
|
+
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
18194
|
);
|
|
18097
18195
|
process.exit(1);
|
|
18098
18196
|
}
|
|
@@ -18155,109 +18253,485 @@ function startCommand() {
|
|
|
18155
18253
|
|
|
18156
18254
|
// src/commands/stop.ts
|
|
18157
18255
|
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";
|
|
18256
|
+
import { Command as Command4 } from "commander";
|
|
18222
18257
|
import chalk4 from "chalk";
|
|
18223
|
-
import { readFileSync as readFileSync7, statSync as statSync3, readdirSync as readdirSync2, openSync, readSync, closeSync } from "fs";
|
|
18224
18258
|
import { execSync as execSync2 } from "child_process";
|
|
18225
|
-
import { resolve as resolve2, join as join7, extname as extname2, basename } from "path";
|
|
18226
18259
|
|
|
18227
|
-
// src/
|
|
18260
|
+
// src/commands/doctor.ts
|
|
18228
18261
|
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
|
-
};
|
|
18248
|
-
}
|
|
18249
|
-
function safeLocator(m) {
|
|
18250
|
-
const len = Math.max(0, m.end - m.start);
|
|
18251
|
-
if (m.deobfuscated && len === 0) return "len=0 unmapped";
|
|
18252
|
-
const pos = m.line !== void 0 && m.column !== void 0 ? `L${m.line}:${m.column} ` : "";
|
|
18253
|
-
return `${pos}len=${len}${m.deobfuscated ? " enc" : ""}`;
|
|
18262
|
+
import { Command as Command3 } from "commander";
|
|
18263
|
+
import chalk3 from "chalk";
|
|
18264
|
+
|
|
18265
|
+
// src/plan-usage.ts
|
|
18266
|
+
init_esm_shims();
|
|
18267
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
18268
|
+
import { join as join7 } from "path";
|
|
18269
|
+
import { homedir as homedir4 } from "os";
|
|
18270
|
+
function credentialsPath() {
|
|
18271
|
+
return join7(homedir4(), ".pretense", "credentials.json");
|
|
18254
18272
|
}
|
|
18255
|
-
|
|
18256
|
-
|
|
18257
|
-
|
|
18258
|
-
|
|
18259
|
-
|
|
18260
|
-
|
|
18273
|
+
function readApiKey() {
|
|
18274
|
+
const fromEnv = process.env["PRETENSE_API_KEY"]?.trim();
|
|
18275
|
+
if (fromEnv) return fromEnv;
|
|
18276
|
+
const path2 = credentialsPath();
|
|
18277
|
+
if (!existsSync8(path2)) return null;
|
|
18278
|
+
try {
|
|
18279
|
+
const creds = JSON.parse(readFileSync7(path2, "utf-8"));
|
|
18280
|
+
const key = creds["apiKey"] ?? creds["api_key"];
|
|
18281
|
+
if (typeof key === "string" && key.trim().length > 0) return key.trim();
|
|
18282
|
+
} catch {
|
|
18283
|
+
}
|
|
18284
|
+
return null;
|
|
18285
|
+
}
|
|
18286
|
+
var DEFAULT_PRETENSE_API_URL = "https://api.pretense.ai";
|
|
18287
|
+
var DEFAULT_TIER_TIMEOUT_MS = 5e3;
|
|
18288
|
+
function getConfiguredApiRoot() {
|
|
18289
|
+
const raw2 = (process.env["PRETENSE_API_URL"] ?? DEFAULT_PRETENSE_API_URL).trim();
|
|
18290
|
+
if (!raw2) return DEFAULT_PRETENSE_API_URL;
|
|
18291
|
+
const normalized = /^https?:\/\//i.test(raw2) ? raw2 : `https://${raw2}`;
|
|
18292
|
+
try {
|
|
18293
|
+
const u = new URL(normalized);
|
|
18294
|
+
const origin = /^pretense\.ai$/i.test(u.hostname) ? DEFAULT_PRETENSE_API_URL : u.origin;
|
|
18295
|
+
const path2 = u.pathname === "/" ? "" : u.pathname.replace(/\/$/, "");
|
|
18296
|
+
return `${origin}${path2}`;
|
|
18297
|
+
} catch {
|
|
18298
|
+
return DEFAULT_PRETENSE_API_URL;
|
|
18299
|
+
}
|
|
18300
|
+
}
|
|
18301
|
+
function tierRequestTimeoutMs() {
|
|
18302
|
+
const raw2 = process.env["PRETENSE_API_TIMEOUT_MS"]?.trim();
|
|
18303
|
+
if (!raw2 || !/^\d+$/.test(raw2)) return DEFAULT_TIER_TIMEOUT_MS;
|
|
18304
|
+
return Math.min(Math.max(parseInt(raw2, 10), 1e3), 12e4);
|
|
18305
|
+
}
|
|
18306
|
+
function planToTier(plan) {
|
|
18307
|
+
switch (String(plan).toUpperCase()) {
|
|
18308
|
+
case "FREE":
|
|
18309
|
+
return "free";
|
|
18310
|
+
case "PRO":
|
|
18311
|
+
return "pro";
|
|
18312
|
+
case "ENTERPRISE":
|
|
18313
|
+
return "enterprise";
|
|
18314
|
+
default:
|
|
18315
|
+
return null;
|
|
18316
|
+
}
|
|
18317
|
+
}
|
|
18318
|
+
async function tierFromBackend(apiKey) {
|
|
18319
|
+
const url = `${getConfiguredApiRoot()}/api/cli/auth/validate`;
|
|
18320
|
+
const controller = new AbortController();
|
|
18321
|
+
const timer = setTimeout(() => controller.abort(), tierRequestTimeoutMs());
|
|
18322
|
+
try {
|
|
18323
|
+
const resp = await fetch(url, {
|
|
18324
|
+
method: "POST",
|
|
18325
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
18326
|
+
body: "{}",
|
|
18327
|
+
signal: controller.signal
|
|
18328
|
+
});
|
|
18329
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
18330
|
+
return {
|
|
18331
|
+
tier: null,
|
|
18332
|
+
source: "unverified",
|
|
18333
|
+
detail: "the server rejected this API key \u2014 run `pretense auth login` again"
|
|
18334
|
+
};
|
|
18335
|
+
}
|
|
18336
|
+
if (!resp.ok) {
|
|
18337
|
+
return {
|
|
18338
|
+
tier: null,
|
|
18339
|
+
source: "unverified",
|
|
18340
|
+
detail: `the plan service returned HTTP ${resp.status}`
|
|
18341
|
+
};
|
|
18342
|
+
}
|
|
18343
|
+
const body = await resp.json();
|
|
18344
|
+
const tier = planToTier(body["plan"]);
|
|
18345
|
+
if (body["valid"] === false || !tier) {
|
|
18346
|
+
return {
|
|
18347
|
+
tier: null,
|
|
18348
|
+
source: "unverified",
|
|
18349
|
+
detail: "the plan service did not report a recognizable plan"
|
|
18350
|
+
};
|
|
18351
|
+
}
|
|
18352
|
+
return { tier, source: "backend" };
|
|
18353
|
+
} catch (err) {
|
|
18354
|
+
const aborted = err?.name === "AbortError";
|
|
18355
|
+
return {
|
|
18356
|
+
tier: null,
|
|
18357
|
+
source: "unverified",
|
|
18358
|
+
detail: aborted ? `could not reach ${url} within ${tierRequestTimeoutMs()}ms` : `could not reach ${url}`
|
|
18359
|
+
};
|
|
18360
|
+
} finally {
|
|
18361
|
+
clearTimeout(timer);
|
|
18362
|
+
}
|
|
18363
|
+
}
|
|
18364
|
+
async function resolvePlanTier(opts = {}) {
|
|
18365
|
+
const apiKey = readApiKey();
|
|
18366
|
+
if (!apiKey) return { tier: "free", source: "no-credentials" };
|
|
18367
|
+
const fromPrefix = tierFromApiKey(apiKey);
|
|
18368
|
+
if (fromPrefix) return { tier: fromPrefix, source: "key-prefix" };
|
|
18369
|
+
if (opts.offline) {
|
|
18370
|
+
return {
|
|
18371
|
+
tier: null,
|
|
18372
|
+
source: "unverified",
|
|
18373
|
+
detail: "--offline was set, and a dashboard key carries no plan of its own"
|
|
18374
|
+
};
|
|
18375
|
+
}
|
|
18376
|
+
return tierFromBackend(apiKey);
|
|
18377
|
+
}
|
|
18378
|
+
var QUOTA_PERIOD_DAYS2 = 7;
|
|
18379
|
+
function collectUsage(dbPath) {
|
|
18380
|
+
const now = /* @__PURE__ */ new Date();
|
|
18381
|
+
const periodMs = QUOTA_PERIOD_DAYS2 * 24 * 60 * 60 * 1e3;
|
|
18382
|
+
const periodStartTs = Date.now() - periodMs;
|
|
18383
|
+
const sinceMs = periodMs;
|
|
18384
|
+
let store = null;
|
|
18385
|
+
try {
|
|
18386
|
+
store = new AuditStore(dbPath);
|
|
18387
|
+
const stats = store.getStats(sinceMs);
|
|
18388
|
+
return {
|
|
18389
|
+
periodStartTs,
|
|
18390
|
+
metrics: {
|
|
18391
|
+
tenantId: "local",
|
|
18392
|
+
// The trailing window these numbers actually cover — previously the
|
|
18393
|
+
// calendar month, which did not match the aggregation OR the quota.
|
|
18394
|
+
periodStart: new Date(periodStartTs).toISOString().split("T")[0],
|
|
18395
|
+
periodEnd: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
|
|
18396
|
+
mutationsApplied: stats.totalMutationsApplied,
|
|
18397
|
+
secretsBlocked: stats.totalSecretsBlocked,
|
|
18398
|
+
piiRedacted: stats.totalPiiRedacted,
|
|
18399
|
+
totalScans: stats.totalRequests,
|
|
18400
|
+
bytesProtected: 0,
|
|
18401
|
+
activeSeats: 1,
|
|
18402
|
+
providers: Object.fromEntries(stats.topProviders.map((p) => [p.provider, p.count]))
|
|
18403
|
+
}
|
|
18404
|
+
};
|
|
18405
|
+
} catch {
|
|
18406
|
+
return null;
|
|
18407
|
+
} finally {
|
|
18408
|
+
store?.close();
|
|
18409
|
+
}
|
|
18410
|
+
}
|
|
18411
|
+
function resetCadenceLabel() {
|
|
18412
|
+
return `every ${QUOTA_PERIOD_DAYS2} days`;
|
|
18413
|
+
}
|
|
18414
|
+
function priceLabel(tier) {
|
|
18415
|
+
const cents = PLAN_CONFIGS[tier].pricePerSeatCents;
|
|
18416
|
+
if (cents === null) return "custom \u2014 contact sales";
|
|
18417
|
+
return cents === 0 ? "free" : `$${cents / 100}/seat/month`;
|
|
18418
|
+
}
|
|
18419
|
+
function tierName(tier) {
|
|
18420
|
+
return { free: "Free", pro: "Pro", enterprise: "Enterprise" }[tier];
|
|
18421
|
+
}
|
|
18422
|
+
function tierLabel(tier) {
|
|
18423
|
+
if (tier === null) return "Unknown (unverified)";
|
|
18424
|
+
if (tier === "free") return "Free";
|
|
18425
|
+
return `${tierName(tier)} (${priceLabel(tier)})`;
|
|
18426
|
+
}
|
|
18427
|
+
function planMismatchHint(resolution) {
|
|
18428
|
+
if (resolution.tier !== "free" || resolution.source !== "backend") return [];
|
|
18429
|
+
return [
|
|
18430
|
+
"Not the plan you expected? An org flagged PRO without an active",
|
|
18431
|
+
"subscription/comp resolves to Free \u2014 see pretense.ai/dashboard/settings or contact support."
|
|
18432
|
+
];
|
|
18433
|
+
}
|
|
18434
|
+
function unverifiedNote(detail) {
|
|
18435
|
+
return [
|
|
18436
|
+
`Could not confirm your plan: ${detail ?? "unknown reason"}.`,
|
|
18437
|
+
"Limits are not shown because guessing could under- or over-state what you pay for."
|
|
18438
|
+
];
|
|
18439
|
+
}
|
|
18440
|
+
|
|
18441
|
+
// src/commands/doctor.ts
|
|
18442
|
+
var BASE_URL_ENVS = [
|
|
18443
|
+
{ name: "ANTHROPIC_BASE_URL", provider: "Claude / Claude Code" },
|
|
18444
|
+
{ name: "OPENAI_BASE_URL", provider: "OpenAI / Cursor / any OpenAI-compatible" },
|
|
18445
|
+
{ name: "GEMINI_BASE_URL", provider: "Google Gemini" },
|
|
18446
|
+
{ name: "GOOGLE_GEMINI_BASE_URL", provider: "Google Gemini (legacy var)" }
|
|
18447
|
+
];
|
|
18448
|
+
var COMMON_PORTS = Array.from({ length: 7 }, (_, i) => 9339 + i);
|
|
18449
|
+
async function probePort(port, timeoutMs = 700) {
|
|
18450
|
+
const ctrl = new AbortController();
|
|
18451
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
18452
|
+
try {
|
|
18453
|
+
const res = await fetch(`http://localhost:${port}/health`, { signal: ctrl.signal });
|
|
18454
|
+
if (!res.ok) return { alive: true, isPretense: false };
|
|
18455
|
+
const hasHeader = res.headers.has("x-pretense-version");
|
|
18456
|
+
let body = {};
|
|
18457
|
+
try {
|
|
18458
|
+
body = await res.json();
|
|
18459
|
+
} catch {
|
|
18460
|
+
return { alive: true, isPretense: false };
|
|
18461
|
+
}
|
|
18462
|
+
const looksPretense = hasHeader && "auditAvailable" in body && "version" in body;
|
|
18463
|
+
return {
|
|
18464
|
+
alive: true,
|
|
18465
|
+
isPretense: looksPretense,
|
|
18466
|
+
status: typeof body["status"] === "string" ? body["status"] : void 0,
|
|
18467
|
+
auditAvailable: typeof body["auditAvailable"] === "boolean" ? body["auditAvailable"] : void 0
|
|
18468
|
+
};
|
|
18469
|
+
} catch {
|
|
18470
|
+
return { alive: false, isPretense: false };
|
|
18471
|
+
} finally {
|
|
18472
|
+
clearTimeout(t);
|
|
18473
|
+
}
|
|
18474
|
+
}
|
|
18475
|
+
function portFromBaseUrl(value) {
|
|
18476
|
+
try {
|
|
18477
|
+
const u = new URL(value);
|
|
18478
|
+
if (u.port) return parseInt(u.port, 10);
|
|
18479
|
+
return u.protocol === "https:" ? 443 : 80;
|
|
18480
|
+
} catch {
|
|
18481
|
+
return null;
|
|
18482
|
+
}
|
|
18483
|
+
}
|
|
18484
|
+
function doctorCommand() {
|
|
18485
|
+
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) => {
|
|
18486
|
+
let problems = 0;
|
|
18487
|
+
const note = (s) => console.log(s);
|
|
18488
|
+
console.log(chalk3.bold.cyan("\n Pretense Doctor\n"));
|
|
18489
|
+
note(chalk3.bold(" Proxy"));
|
|
18490
|
+
const record = readProxyRecord(pidFilePath());
|
|
18491
|
+
let recordedPort = null;
|
|
18492
|
+
if (record) {
|
|
18493
|
+
if (isProcessAlive(record.pid)) {
|
|
18494
|
+
const probe = await probePort(record.port);
|
|
18495
|
+
if (probe.isPretense) {
|
|
18496
|
+
recordedPort = record.port;
|
|
18497
|
+
const degraded = probe.status === "degraded" || probe.auditAvailable === false;
|
|
18498
|
+
note(chalk3.green(` \u2713 Pretense proxy running on :${record.port} (pid ${record.pid}).`));
|
|
18499
|
+
if (degraded) {
|
|
18500
|
+
note(chalk3.yellow(` \u26A0 Audit store unavailable \u2014 requests are still filtered, but ` + chalk3.cyan("pretense audit") + chalk3.yellow(" will be empty this session.")));
|
|
18501
|
+
}
|
|
18502
|
+
} else if (probe.alive) {
|
|
18503
|
+
problems += 1;
|
|
18504
|
+
note(chalk3.yellow(` \u26A0 Pidfile says a proxy is on :${record.port} (pid ${record.pid}), but that port is answering as something ELSE.`));
|
|
18505
|
+
note(chalk3.dim(` Something took the port. Stop the stray process, then ` + chalk3.cyan("pretense start") + chalk3.dim(".")));
|
|
18506
|
+
} else {
|
|
18507
|
+
problems += 1;
|
|
18508
|
+
note(chalk3.yellow(` \u26A0 Pidfile records pid ${record.pid} on :${record.port}, but it is not answering /health.`));
|
|
18509
|
+
note(chalk3.dim(" It may be starting, wedged, or dead. Try ") + chalk3.cyan("pretense stop") + chalk3.dim(" then ") + chalk3.cyan("pretense start") + chalk3.dim("."));
|
|
18510
|
+
}
|
|
18511
|
+
} else {
|
|
18512
|
+
problems += 1;
|
|
18513
|
+
note(chalk3.yellow(` \u26A0 Stale pidfile: pid ${record.pid} (port ${record.port}) is no longer alive.`));
|
|
18514
|
+
note(chalk3.dim(" Harmless, cleared on the next ") + chalk3.cyan("pretense start") + chalk3.dim("."));
|
|
18515
|
+
}
|
|
18516
|
+
} else {
|
|
18517
|
+
note(chalk3.dim(" \u25CB No Pretense proxy recorded (pidfile absent)."));
|
|
18518
|
+
}
|
|
18519
|
+
const stray = [];
|
|
18520
|
+
for (const p of COMMON_PORTS) {
|
|
18521
|
+
if (p === recordedPort) continue;
|
|
18522
|
+
const probe = await probePort(p, 400);
|
|
18523
|
+
if (probe.isPretense) stray.push(p);
|
|
18524
|
+
}
|
|
18525
|
+
if (stray.length > 0) {
|
|
18526
|
+
note(chalk3.yellow(` \u26A0 Other Pretense proxies answering on: ${stray.map((p) => `:${p}`).join(", ")}.`));
|
|
18527
|
+
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."));
|
|
18528
|
+
}
|
|
18529
|
+
note(chalk3.bold("\n This shell"));
|
|
18530
|
+
const setEnvs = BASE_URL_ENVS.filter((e) => (process.env[e.name] ?? "").trim() !== "");
|
|
18531
|
+
let protectedHere = false;
|
|
18532
|
+
if (setEnvs.length === 0) {
|
|
18533
|
+
problems += 1;
|
|
18534
|
+
note(chalk3.red(" \u2717 No base-URL env var is set in THIS terminal \u2014 your AI tool here is NOT protected."));
|
|
18535
|
+
note(chalk3.dim(" Env vars are per-shell: a proxy running elsewhere does not protect this terminal."));
|
|
18536
|
+
note(chalk3.dim(" Simplest fix (no export, no second terminal):"));
|
|
18537
|
+
note(" " + chalk3.cyan('pretense run claude "\u2026"') + chalk3.dim(" # or cursor, gemini, any tool"));
|
|
18538
|
+
if (recordedPort !== null) {
|
|
18539
|
+
note(chalk3.dim(" Or, IN THIS terminal:"));
|
|
18540
|
+
note(" " + chalk3.cyan(`export ANTHROPIC_BASE_URL=http://localhost:${recordedPort}`));
|
|
18541
|
+
}
|
|
18542
|
+
} else {
|
|
18543
|
+
for (const e of setEnvs) {
|
|
18544
|
+
const val = (process.env[e.name] ?? "").trim();
|
|
18545
|
+
const port = portFromBaseUrl(val);
|
|
18546
|
+
if (port === null) {
|
|
18547
|
+
problems += 1;
|
|
18548
|
+
note(chalk3.red(` \u2717 ${e.name}=${val} is not a valid URL.`));
|
|
18549
|
+
continue;
|
|
18550
|
+
}
|
|
18551
|
+
const probe = await probePort(port);
|
|
18552
|
+
if (probe.isPretense) {
|
|
18553
|
+
protectedHere = true;
|
|
18554
|
+
note(chalk3.green(` \u2713 ${e.name} \u2192 :${port} is a live Pretense proxy (${e.provider}).`));
|
|
18555
|
+
} else if (probe.alive) {
|
|
18556
|
+
problems += 1;
|
|
18557
|
+
note(chalk3.red(` \u2717 ${e.name} \u2192 :${port} is answering, but it is NOT a Pretense proxy.`));
|
|
18558
|
+
note(chalk3.dim(" Your traffic is going somewhere, but not through the firewall. Fix the URL or use ") + chalk3.cyan("pretense run") + chalk3.dim("."));
|
|
18559
|
+
} else {
|
|
18560
|
+
problems += 1;
|
|
18561
|
+
note(chalk3.red(` \u2717 ${e.name} \u2192 :${port} is set, but nothing is listening there.`));
|
|
18562
|
+
note(chalk3.dim(" Start the proxy (") + chalk3.cyan("pretense start") + chalk3.dim(") or, simplest, use ") + chalk3.cyan("pretense run") + chalk3.dim("."));
|
|
18563
|
+
}
|
|
18564
|
+
}
|
|
18565
|
+
}
|
|
18566
|
+
note(chalk3.bold("\n Plan"));
|
|
18567
|
+
try {
|
|
18568
|
+
const resolution = await resolvePlanTier({ offline: opts.offline });
|
|
18569
|
+
note(chalk3.dim(` Plan: ${tierLabel(resolution.tier)}`));
|
|
18570
|
+
for (const line of planMismatchHint(resolution)) {
|
|
18571
|
+
note(chalk3.dim(` ${line}`));
|
|
18572
|
+
}
|
|
18573
|
+
} catch {
|
|
18574
|
+
note(chalk3.dim(" Plan: could not be resolved (offline or no credentials)."));
|
|
18575
|
+
}
|
|
18576
|
+
if (protectedHere && problems === 0) {
|
|
18577
|
+
note(chalk3.bold.green("\n \u2713 This shell is protected \u2014 traffic routes through Pretense.\n"));
|
|
18578
|
+
process.exitCode = 0;
|
|
18579
|
+
} else if (protectedHere) {
|
|
18580
|
+
note(chalk3.bold.yellow(`
|
|
18581
|
+
\u26A0 This shell is protected, but doctor found ${problems} thing(s) to look at above.
|
|
18582
|
+
`));
|
|
18583
|
+
process.exitCode = 1;
|
|
18584
|
+
} else {
|
|
18585
|
+
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"));
|
|
18586
|
+
process.exitCode = 1;
|
|
18587
|
+
}
|
|
18588
|
+
});
|
|
18589
|
+
}
|
|
18590
|
+
|
|
18591
|
+
// src/commands/stop.ts
|
|
18592
|
+
function pidListeningOn2(port) {
|
|
18593
|
+
try {
|
|
18594
|
+
const out = execSync2(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`, {
|
|
18595
|
+
encoding: "utf-8",
|
|
18596
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
18597
|
+
}).trim();
|
|
18598
|
+
const pid = parseInt(out.split("\n")[0] ?? "", 10);
|
|
18599
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
18600
|
+
} catch {
|
|
18601
|
+
return null;
|
|
18602
|
+
}
|
|
18603
|
+
}
|
|
18604
|
+
async function isRecordOurs(record) {
|
|
18605
|
+
const port = record.port;
|
|
18606
|
+
if (port > 0) {
|
|
18607
|
+
const probe = await probePort(port, 700);
|
|
18608
|
+
const listener = pidListeningOn2(port);
|
|
18609
|
+
if (probe.isPretense) {
|
|
18610
|
+
return listener === null || listener === record.pid;
|
|
18611
|
+
}
|
|
18612
|
+
return listener !== null && listener === record.pid;
|
|
18613
|
+
}
|
|
18614
|
+
return true;
|
|
18615
|
+
}
|
|
18616
|
+
function stopCommand() {
|
|
18617
|
+
return new Command4("stop").description("Stop the running Pretense proxy").option("--json", "Machine-readable outcome", false).action(async (opts) => {
|
|
18618
|
+
const path2 = pidFilePath();
|
|
18619
|
+
const record = readProxyRecord(path2);
|
|
18620
|
+
if (record && isProcessAlive(record.pid) && !await isRecordOurs(record)) {
|
|
18621
|
+
removeProxyRecord(path2);
|
|
18622
|
+
const outcome2 = { kind: "foreign", pid: record.pid, port: record.port };
|
|
18623
|
+
if (opts.json) {
|
|
18624
|
+
console.log(JSON.stringify(outcome2, null, 2));
|
|
18625
|
+
process.exit(0);
|
|
18626
|
+
}
|
|
18627
|
+
console.log(
|
|
18628
|
+
chalk4.yellow(`
|
|
18629
|
+
\xB7 Not stopping pid ${record.pid}: it is not the Pretense proxy.`) + chalk4.dim(`
|
|
18630
|
+
The recorded proxy (port ${record.port}) is gone and that pid now`) + chalk4.dim(`
|
|
18631
|
+
belongs to an unrelated process \u2014 Pretense will not signal it.`) + chalk4.dim(`
|
|
18632
|
+
Cleaned up the stale record; nothing was killed.
|
|
18633
|
+
`)
|
|
18634
|
+
);
|
|
18635
|
+
process.exit(0);
|
|
18636
|
+
}
|
|
18637
|
+
const outcome = await stopProxy(path2);
|
|
18638
|
+
if (opts.json) {
|
|
18639
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
18640
|
+
process.exit(outcome.kind === "refused" || outcome.kind === "not-permitted" ? 1 : 0);
|
|
18641
|
+
}
|
|
18642
|
+
switch (outcome.kind) {
|
|
18643
|
+
case "not-running":
|
|
18644
|
+
console.log(
|
|
18645
|
+
chalk4.dim(`
|
|
18646
|
+
\xB7 No running Pretense proxy recorded (${path2} does not exist).`) + chalk4.dim(`
|
|
18647
|
+
Start one with `) + chalk4.cyan("pretense start") + "\n"
|
|
18648
|
+
);
|
|
18649
|
+
process.exit(0);
|
|
18650
|
+
break;
|
|
18651
|
+
case "stale":
|
|
18652
|
+
console.log(
|
|
18653
|
+
chalk4.yellow(
|
|
18654
|
+
`
|
|
18655
|
+
\xB7 Stale PID file cleaned up${outcome.pid ? ` (pid ${outcome.pid} is not running)` : ""}.`
|
|
18656
|
+
) + chalk4.dim(`
|
|
18657
|
+
No proxy was running.
|
|
18658
|
+
`)
|
|
18659
|
+
);
|
|
18660
|
+
process.exit(0);
|
|
18661
|
+
break;
|
|
18662
|
+
case "stopped":
|
|
18663
|
+
console.log(
|
|
18664
|
+
chalk4.green(
|
|
18665
|
+
`
|
|
18666
|
+
\u2713 Pretense proxy stopped (pid ${outcome.pid}${outcome.port ? `, port ${outcome.port}` : ""})` + (outcome.forced ? " \u2014 did not exit on SIGTERM, sent SIGKILL" : "") + "\n"
|
|
18667
|
+
)
|
|
18668
|
+
);
|
|
18669
|
+
process.exit(0);
|
|
18670
|
+
break;
|
|
18671
|
+
case "not-permitted":
|
|
18672
|
+
console.error(
|
|
18673
|
+
chalk4.red(`
|
|
18674
|
+
x Cannot stop pid ${outcome.pid}: it belongs to another user.`) + chalk4.dim(`
|
|
18675
|
+
The proxy is still running. Stop it as its owner, or: sudo kill ${outcome.pid}
|
|
18676
|
+
`)
|
|
18677
|
+
);
|
|
18678
|
+
process.exit(1);
|
|
18679
|
+
break;
|
|
18680
|
+
case "refused":
|
|
18681
|
+
console.error(
|
|
18682
|
+
chalk4.red(`
|
|
18683
|
+
x Pid ${outcome.pid} is still alive after SIGTERM and SIGKILL.`) + chalk4.dim(`
|
|
18684
|
+
${path2} was left in place so you can investigate: ps -p ${outcome.pid}
|
|
18685
|
+
`)
|
|
18686
|
+
);
|
|
18687
|
+
process.exit(1);
|
|
18688
|
+
break;
|
|
18689
|
+
}
|
|
18690
|
+
});
|
|
18691
|
+
}
|
|
18692
|
+
|
|
18693
|
+
// src/commands/scan.ts
|
|
18694
|
+
init_esm_shims();
|
|
18695
|
+
import { Command as Command5, Option, InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
18696
|
+
import chalk5 from "chalk";
|
|
18697
|
+
import { readFileSync as readFileSync8, statSync as statSync3, readdirSync as readdirSync2, openSync, readSync, closeSync } from "fs";
|
|
18698
|
+
import { execSync as execSync3 } from "child_process";
|
|
18699
|
+
import { resolve as resolve3, join as join8, extname as extname2, basename } from "path";
|
|
18700
|
+
|
|
18701
|
+
// src/safe-output.ts
|
|
18702
|
+
init_esm_shims();
|
|
18703
|
+
var UNSAFE_VALUES_FLAG = "--unsafe-show-values";
|
|
18704
|
+
function lineColFromOffset2(text, offset) {
|
|
18705
|
+
const clamped = Math.max(0, Math.min(offset, text.length));
|
|
18706
|
+
let line = 1;
|
|
18707
|
+
let lineStart = 0;
|
|
18708
|
+
for (let i = 0; i < clamped; i++) {
|
|
18709
|
+
if (text.charCodeAt(i) === 10) {
|
|
18710
|
+
line++;
|
|
18711
|
+
lineStart = i + 1;
|
|
18712
|
+
}
|
|
18713
|
+
}
|
|
18714
|
+
return { line, column: clamped - lineStart + 1 };
|
|
18715
|
+
}
|
|
18716
|
+
function withLocations(result, content) {
|
|
18717
|
+
const locate = (m) => lineColFromOffset2(content, m.start);
|
|
18718
|
+
return {
|
|
18719
|
+
...result,
|
|
18720
|
+
matches: result.matches.map((m) => ({ ...m, ...locate(m) }))
|
|
18721
|
+
};
|
|
18722
|
+
}
|
|
18723
|
+
function safeLocator(m) {
|
|
18724
|
+
const len = Math.max(0, m.end - m.start);
|
|
18725
|
+
if (m.deobfuscated && len === 0) return "len=0 unmapped";
|
|
18726
|
+
const pos = m.line !== void 0 && m.column !== void 0 ? `L${m.line}:${m.column} ` : "";
|
|
18727
|
+
return `${pos}len=${len}${m.deobfuscated ? " enc" : ""}`;
|
|
18728
|
+
}
|
|
18729
|
+
var VALUE_FIELDS = ["value", "masked", "deobfuscatedValue"];
|
|
18730
|
+
function stripRow(row) {
|
|
18731
|
+
const out = {};
|
|
18732
|
+
for (const [k, v] of Object.entries(row)) {
|
|
18733
|
+
if (VALUE_FIELDS.includes(k)) continue;
|
|
18734
|
+
out[k] = v;
|
|
18261
18735
|
}
|
|
18262
18736
|
out.length = typeof row.end === "number" && typeof row.start === "number" ? row.end - row.start : 0;
|
|
18263
18737
|
return out;
|
|
@@ -18349,7 +18823,7 @@ function isBinaryFile(filePath) {
|
|
|
18349
18823
|
}
|
|
18350
18824
|
var SEVERITY_ORDER = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
18351
18825
|
function collectFiles(target) {
|
|
18352
|
-
const abs =
|
|
18826
|
+
const abs = resolve3(target);
|
|
18353
18827
|
let stat;
|
|
18354
18828
|
try {
|
|
18355
18829
|
stat = statSync3(abs);
|
|
@@ -18404,7 +18878,7 @@ function walkDir(dir) {
|
|
|
18404
18878
|
} catch (err) {
|
|
18405
18879
|
if (isSkippableWalkError(err)) {
|
|
18406
18880
|
const code = err.code;
|
|
18407
|
-
console.error(
|
|
18881
|
+
console.error(chalk5.yellow(` Skipping unreadable path (${code}): ${dir}`));
|
|
18408
18882
|
return files;
|
|
18409
18883
|
}
|
|
18410
18884
|
throw err;
|
|
@@ -18412,16 +18886,16 @@ function walkDir(dir) {
|
|
|
18412
18886
|
for (const entry of entries) {
|
|
18413
18887
|
if (entry.isDirectory()) {
|
|
18414
18888
|
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
|
|
18415
|
-
files.push(...walkDir(
|
|
18889
|
+
files.push(...walkDir(join8(dir, entry.name)));
|
|
18416
18890
|
} else if (entry.isFile()) {
|
|
18417
|
-
files.push(
|
|
18891
|
+
files.push(join8(dir, entry.name));
|
|
18418
18892
|
}
|
|
18419
18893
|
}
|
|
18420
18894
|
return files;
|
|
18421
18895
|
}
|
|
18422
18896
|
function gitExecChecked(cmd) {
|
|
18423
18897
|
try {
|
|
18424
|
-
const out =
|
|
18898
|
+
const out = execSync3(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
18425
18899
|
return { ok: true, lines: out ? out.split("\n").filter(Boolean) : [] };
|
|
18426
18900
|
} catch (err) {
|
|
18427
18901
|
const e = err;
|
|
@@ -18468,7 +18942,7 @@ function warnIfLargeForL2(byteLength, filePath) {
|
|
|
18468
18942
|
const mb = (byteLength / (1024 * 1024)).toFixed(1);
|
|
18469
18943
|
const where = filePath ? ` \u2014 ${basename(filePath)}` : "";
|
|
18470
18944
|
console.error(
|
|
18471
|
-
|
|
18945
|
+
chalk5.yellow(
|
|
18472
18946
|
` Note: L2 AST scanning is slow on very large files (${mb}MB${where}); this may take a while.`
|
|
18473
18947
|
)
|
|
18474
18948
|
);
|
|
@@ -18531,14 +19005,14 @@ async function scanContentRaw(content, opts, filePath) {
|
|
|
18531
19005
|
return result;
|
|
18532
19006
|
}
|
|
18533
19007
|
async function scanFile(filePath, opts) {
|
|
18534
|
-
const content =
|
|
19008
|
+
const content = readFileSync8(filePath, "utf-8");
|
|
18535
19009
|
return scanContent(content, opts, filePath);
|
|
18536
19010
|
}
|
|
18537
19011
|
function severityIcon(severity) {
|
|
18538
19012
|
return severity === "critical" ? "\u{1F534}" : severity === "high" ? "\u{1F7E0}" : severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
|
|
18539
19013
|
}
|
|
18540
19014
|
function levelTag(level) {
|
|
18541
|
-
return level === 2 ?
|
|
19015
|
+
return level === 2 ? chalk5.magenta("L2") : chalk5.blue("L1");
|
|
18542
19016
|
}
|
|
18543
19017
|
function rowTail(match3, opts) {
|
|
18544
19018
|
if (opts.unsafeShowValues) return match3.masked;
|
|
@@ -18549,12 +19023,12 @@ function emitResult(result, opts) {
|
|
|
18549
19023
|
}
|
|
18550
19024
|
function printFindings(filePath, result, opts) {
|
|
18551
19025
|
if (result.clean) return;
|
|
18552
|
-
console.log(
|
|
19026
|
+
console.log(chalk5.underline(`
|
|
18553
19027
|
${filePath}`));
|
|
18554
19028
|
for (const match3 of result.matches) {
|
|
18555
|
-
const actionColor = match3.action === "block" ?
|
|
19029
|
+
const actionColor = match3.action === "block" ? chalk5.red : match3.action === "redact" ? chalk5.yellow : chalk5.dim;
|
|
18556
19030
|
console.log(
|
|
18557
|
-
` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${
|
|
19031
|
+
` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk5.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk5.dim(rowTail(match3, opts))}`
|
|
18558
19032
|
);
|
|
18559
19033
|
}
|
|
18560
19034
|
}
|
|
@@ -18640,7 +19114,7 @@ async function scanFiles(files, opts) {
|
|
|
18640
19114
|
let blocked = 0;
|
|
18641
19115
|
for (const f of files) {
|
|
18642
19116
|
if (isBinaryFile(f)) {
|
|
18643
|
-
console.error(
|
|
19117
|
+
console.error(chalk5.dim(` Skipping binary file: ${f}`));
|
|
18644
19118
|
continue;
|
|
18645
19119
|
}
|
|
18646
19120
|
try {
|
|
@@ -18682,17 +19156,17 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
|
|
|
18682
19156
|
}
|
|
18683
19157
|
if (files.length === 0) {
|
|
18684
19158
|
console.error(
|
|
18685
|
-
|
|
18686
|
-
No supported files found in ${label}.`) +
|
|
19159
|
+
chalk5.yellow(`
|
|
19160
|
+
No supported files found in ${label}.`) + chalk5.dim(`
|
|
18687
19161
|
Supported: ${SUPPORTED_EXTENSIONS_DISPLAY}
|
|
18688
19162
|
`)
|
|
18689
19163
|
);
|
|
18690
19164
|
} else {
|
|
18691
19165
|
console.log(
|
|
18692
|
-
|
|
18693
|
-
No supported source files found.`) +
|
|
19166
|
+
chalk5.yellow(`
|
|
19167
|
+
No supported source files found.`) + chalk5.dim(`
|
|
18694
19168
|
Currently supports: TypeScript, JavaScript, Python, Go, Java, C#, Ruby, Rust
|
|
18695
|
-
`) +
|
|
19169
|
+
`) + chalk5.dim(` (${SUPPORTED_EXTENSIONS_DISPLAY})
|
|
18696
19170
|
`)
|
|
18697
19171
|
);
|
|
18698
19172
|
}
|
|
@@ -18723,18 +19197,18 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
|
|
|
18723
19197
|
if (allResults.length === 0) {
|
|
18724
19198
|
if (notice) console.log(notice);
|
|
18725
19199
|
if (policyLine) console.log(policyLine);
|
|
18726
|
-
console.log(
|
|
19200
|
+
console.log(chalk5.green(` \u2713 No secrets or PII detected`));
|
|
18727
19201
|
process.exit(0);
|
|
18728
19202
|
}
|
|
18729
|
-
console.log(
|
|
19203
|
+
console.log(chalk5.cyan(`
|
|
18730
19204
|
Pretense Scan \u2014 ${label}
|
|
18731
19205
|
`));
|
|
18732
|
-
console.log(
|
|
19206
|
+
console.log(chalk5.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)}
|
|
18733
19207
|
`));
|
|
18734
19208
|
if (policyLine) console.log(policyLine + "\n");
|
|
18735
19209
|
if (notice) console.log(notice + "\n");
|
|
18736
19210
|
for (const { file, result } of allResults) printFindings(file, result, opts);
|
|
18737
|
-
console.log(
|
|
19211
|
+
console.log(chalk5.dim(`
|
|
18738
19212
|
Total: ${total} findings | ${blocked} blocked
|
|
18739
19213
|
`));
|
|
18740
19214
|
process.exit(findingsExitCode);
|
|
@@ -18764,7 +19238,7 @@ function levelOption() {
|
|
|
18764
19238
|
function parseLevel(v) {
|
|
18765
19239
|
if (v === "1") return 1;
|
|
18766
19240
|
if (v === "2") return 2;
|
|
18767
|
-
throw new
|
|
19241
|
+
throw new InvalidArgumentError2("Detection level must be 1 (regex) or 2 (AST).");
|
|
18768
19242
|
}
|
|
18769
19243
|
function levelsLabel(opts) {
|
|
18770
19244
|
if (opts.level === 1) return "L1 only (--level 1)";
|
|
@@ -18776,13 +19250,13 @@ function l2Notice(results, opts) {
|
|
|
18776
19250
|
if (isAstCoreAvailable()) {
|
|
18777
19251
|
const errored = results.filter((r) => r.result.l2?.parseErrors).length;
|
|
18778
19252
|
if (errored > 0) {
|
|
18779
|
-
return
|
|
19253
|
+
return chalk5.yellow(
|
|
18780
19254
|
` ! L2 parsed ${errored} file(s) with syntax errors \u2014 AST findings may be incomplete. L1 results are unaffected.`
|
|
18781
19255
|
);
|
|
18782
19256
|
}
|
|
18783
19257
|
return null;
|
|
18784
19258
|
}
|
|
18785
|
-
return
|
|
19259
|
+
return chalk5.yellow(
|
|
18786
19260
|
" ! 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
19261
|
);
|
|
18788
19262
|
}
|
|
@@ -18794,9 +19268,9 @@ function validateScanFlags(opts) {
|
|
|
18794
19268
|
const raw2 = String(opts.format).trim().toLowerCase();
|
|
18795
19269
|
if (!VALID_FORMATS.includes(raw2)) {
|
|
18796
19270
|
console.error(
|
|
18797
|
-
|
|
19271
|
+
chalk5.red(`
|
|
18798
19272
|
x Unknown --format: "${opts.format}"
|
|
18799
|
-
`) +
|
|
19273
|
+
`) + chalk5.dim(` Valid formats: ${VALID_FORMATS.join(", ")}.
|
|
18800
19274
|
`)
|
|
18801
19275
|
);
|
|
18802
19276
|
process.exit(USAGE_EXIT);
|
|
@@ -18807,9 +19281,9 @@ function validateScanFlags(opts) {
|
|
|
18807
19281
|
const raw2 = String(opts.severity).trim().toLowerCase();
|
|
18808
19282
|
if (!VALID_SEVERITIES.includes(raw2)) {
|
|
18809
19283
|
console.error(
|
|
18810
|
-
|
|
19284
|
+
chalk5.red(`
|
|
18811
19285
|
x Unknown --severity: "${opts.severity}"
|
|
18812
|
-
`) +
|
|
19286
|
+
`) + chalk5.dim(` Valid severities: ${VALID_SEVERITIES.join(", ")}.
|
|
18813
19287
|
`)
|
|
18814
19288
|
);
|
|
18815
19289
|
process.exit(USAGE_EXIT);
|
|
@@ -18826,7 +19300,7 @@ function applyPolicy(opts) {
|
|
|
18826
19300
|
if (resolved.scannerPreset) opts.presetName = resolved.scannerPreset;
|
|
18827
19301
|
}
|
|
18828
19302
|
} catch (err) {
|
|
18829
|
-
console.error(
|
|
19303
|
+
console.error(chalk5.red(`
|
|
18830
19304
|
x ${err.message}
|
|
18831
19305
|
`));
|
|
18832
19306
|
process.exit(USAGE_EXIT);
|
|
@@ -18836,15 +19310,15 @@ function applyPolicy(opts) {
|
|
|
18836
19310
|
function policyNotice(opts) {
|
|
18837
19311
|
if (!opts.policyResolved) return null;
|
|
18838
19312
|
const p = opts.policyResolved;
|
|
18839
|
-
const line =
|
|
18840
|
-
return p.scannerPreset ? line :
|
|
19313
|
+
const line = chalk5.dim(` ${describePolicy(p)}`);
|
|
19314
|
+
return p.scannerPreset ? line : chalk5.yellow(` ! ${describePolicy(p)}`);
|
|
18841
19315
|
}
|
|
18842
19316
|
function inheritedScanOpts(cmd) {
|
|
18843
19317
|
return cmd.optsWithGlobals();
|
|
18844
19318
|
}
|
|
18845
19319
|
function ciSubcommand() {
|
|
18846
19320
|
return addScanOpts(
|
|
18847
|
-
new
|
|
19321
|
+
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
19322
|
).action(async (target, opts, cmd) => {
|
|
18849
19323
|
opts = inheritedScanOpts(cmd);
|
|
18850
19324
|
applyPolicy(opts);
|
|
@@ -18852,8 +19326,8 @@ function ciSubcommand() {
|
|
|
18852
19326
|
const files2 = collectFiles(target);
|
|
18853
19327
|
if (files2.length === 0) {
|
|
18854
19328
|
console.error(
|
|
18855
|
-
|
|
18856
|
-
x Path not found or empty: ${target}`) +
|
|
19329
|
+
chalk5.red(`
|
|
19330
|
+
x Path not found or empty: ${target}`) + chalk5.dim(`
|
|
18857
19331
|
Try: pretense scan ci <file-or-directory>, or omit the path to scan git-tracked files.
|
|
18858
19332
|
`)
|
|
18859
19333
|
);
|
|
@@ -18865,11 +19339,11 @@ function ciSubcommand() {
|
|
|
18865
19339
|
const tracked = gitExecChecked("git ls-files");
|
|
18866
19340
|
if (!tracked.ok) {
|
|
18867
19341
|
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.`) +
|
|
19342
|
+
chalk5.red(`
|
|
19343
|
+
x scan ci could not enumerate git-tracked files \u2014 NOTHING WAS SCANNED.`) + chalk5.dim(`
|
|
19344
|
+
git: ${tracked.error}`) + chalk5.dim(`
|
|
19345
|
+
This is a scan FAILURE (exit 1), not a clean result. Reporting 0 here`) + chalk5.dim(`
|
|
19346
|
+
would mark a CI gate green over a tree that was never read.`) + chalk5.dim(`
|
|
18873
19347
|
Fix the checkout, or scan a path directly: pretense scan ci <path>
|
|
18874
19348
|
`)
|
|
18875
19349
|
);
|
|
@@ -18885,7 +19359,7 @@ function ciSubcommand() {
|
|
|
18885
19359
|
}
|
|
18886
19360
|
function commitRangeSubcommand() {
|
|
18887
19361
|
return addScanOpts(
|
|
18888
|
-
new
|
|
19362
|
+
new Command5("commit-range").description("Scan files changed between two commits").argument("<range>", "Commit range (e.g. base..head)")
|
|
18889
19363
|
).action(async (range, opts, cmd) => {
|
|
18890
19364
|
opts = inheritedScanOpts(cmd);
|
|
18891
19365
|
applyPolicy(opts);
|
|
@@ -18893,7 +19367,7 @@ function commitRangeSubcommand() {
|
|
|
18893
19367
|
if (files.length === 0) {
|
|
18894
19368
|
const fmt2 = resolveFormat(opts);
|
|
18895
19369
|
if (fmt2 === "json") console.log(JSON.stringify({ clean: true, files: 0, findings: 0 }));
|
|
18896
|
-
else console.log(
|
|
19370
|
+
else console.log(chalk5.green(" \u2713 No changed files in range"));
|
|
18897
19371
|
process.exit(0);
|
|
18898
19372
|
}
|
|
18899
19373
|
await outputMultiFileResults(`commit-range ${range}`, files, opts);
|
|
@@ -18901,7 +19375,7 @@ function commitRangeSubcommand() {
|
|
|
18901
19375
|
}
|
|
18902
19376
|
function preCommitSubcommand() {
|
|
18903
19377
|
return addScanOpts(
|
|
18904
|
-
new
|
|
19378
|
+
new Command5("pre-commit").description("Scan git staged files")
|
|
18905
19379
|
).action(async (opts, cmd) => {
|
|
18906
19380
|
opts = inheritedScanOpts(cmd);
|
|
18907
19381
|
applyPolicy(opts);
|
|
@@ -18909,23 +19383,79 @@ function preCommitSubcommand() {
|
|
|
18909
19383
|
if (files.length === 0) {
|
|
18910
19384
|
const fmt2 = resolveFormat(opts);
|
|
18911
19385
|
if (fmt2 === "json") console.log(JSON.stringify({ clean: true, files: 0, findings: 0 }));
|
|
18912
|
-
else console.log(
|
|
19386
|
+
else console.log(chalk5.green(" \u2713 No staged files to scan"));
|
|
18913
19387
|
process.exit(0);
|
|
18914
19388
|
}
|
|
18915
19389
|
await outputMultiFileResults(`pre-commit (${files.length} staged files)`, files, opts);
|
|
18916
19390
|
});
|
|
18917
19391
|
}
|
|
19392
|
+
function isZeroOid(oid) {
|
|
19393
|
+
return /^0+$/.test(oid.trim());
|
|
19394
|
+
}
|
|
19395
|
+
function emptyTreeOid() {
|
|
19396
|
+
const r = gitExecChecked("git hash-object -t tree /dev/null");
|
|
19397
|
+
return r.ok && r.lines[0] ? r.lines[0] : "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
19398
|
+
}
|
|
19399
|
+
function changedFilesForPush(localSha, remoteSha) {
|
|
19400
|
+
if (isZeroOid(localSha)) return { ok: true, lines: [] };
|
|
19401
|
+
if (!isZeroOid(remoteSha)) {
|
|
19402
|
+
return gitExecChecked(`git diff --name-only ${remoteSha} ${localSha}`);
|
|
19403
|
+
}
|
|
19404
|
+
const revs = gitExecChecked(`git rev-list ${localSha} --not --remotes`);
|
|
19405
|
+
if (revs.ok && revs.lines.length > 0) {
|
|
19406
|
+
const oldest = revs.lines[revs.lines.length - 1];
|
|
19407
|
+
const parent = gitExecChecked(`git rev-parse --verify --quiet ${oldest}^`);
|
|
19408
|
+
const base = parent.ok && parent.lines[0] ? parent.lines[0] : emptyTreeOid();
|
|
19409
|
+
return gitExecChecked(`git diff --name-only ${base} ${localSha}`);
|
|
19410
|
+
}
|
|
19411
|
+
if (!revs.ok) return revs;
|
|
19412
|
+
return gitExecChecked(`git diff --name-only ${emptyTreeOid()} ${localSha}`);
|
|
19413
|
+
}
|
|
19414
|
+
async function readStdin() {
|
|
19415
|
+
if (process.stdin.isTTY) return "";
|
|
19416
|
+
const chunks = [];
|
|
19417
|
+
try {
|
|
19418
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
19419
|
+
} catch {
|
|
19420
|
+
return "";
|
|
19421
|
+
}
|
|
19422
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
19423
|
+
}
|
|
18918
19424
|
function prePushSubcommand() {
|
|
18919
19425
|
return addScanOpts(
|
|
18920
|
-
new
|
|
19426
|
+
new Command5("pre-push").description("Scan the commits being pushed (reads git's pre-push ref lines on stdin)")
|
|
18921
19427
|
).action(async (opts, cmd) => {
|
|
18922
19428
|
opts = inheritedScanOpts(cmd);
|
|
18923
19429
|
applyPolicy(opts);
|
|
18924
|
-
const
|
|
19430
|
+
const stdin = await readStdin();
|
|
19431
|
+
const refLines = stdin.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
19432
|
+
const changed = /* @__PURE__ */ new Set();
|
|
19433
|
+
if (refLines.length > 0) {
|
|
19434
|
+
for (const line of refLines) {
|
|
19435
|
+
const [, localSha, , remoteSha] = line.split(/\s+/);
|
|
19436
|
+
if (!localSha || !remoteSha) continue;
|
|
19437
|
+
const r = changedFilesForPush(localSha, remoteSha);
|
|
19438
|
+
if (!r.ok) {
|
|
19439
|
+
console.error(
|
|
19440
|
+
chalk5.red(`
|
|
19441
|
+
x pre-push: could not determine what is being pushed \u2014 NOTHING WAS SCANNED.`) + chalk5.dim(`
|
|
19442
|
+
git: ${r.error}`) + chalk5.dim(`
|
|
19443
|
+
Blocking the push (exit 1) rather than letting an unscanned commit through.`) + chalk5.dim(`
|
|
19444
|
+
Override at your own risk with: git push --no-verify
|
|
19445
|
+
`)
|
|
19446
|
+
);
|
|
19447
|
+
process.exit(1);
|
|
19448
|
+
}
|
|
19449
|
+
for (const f of r.lines) changed.add(f);
|
|
19450
|
+
}
|
|
19451
|
+
} else {
|
|
19452
|
+
for (const f of gitExec("git diff --name-only @{push}..HEAD")) changed.add(f);
|
|
19453
|
+
}
|
|
19454
|
+
const files = filterExisting([...changed]);
|
|
18925
19455
|
if (files.length === 0) {
|
|
18926
19456
|
const fmt2 = resolveFormat(opts);
|
|
18927
19457
|
if (fmt2 === "json") console.log(JSON.stringify({ clean: true, files: 0, findings: 0 }));
|
|
18928
|
-
else console.log(
|
|
19458
|
+
else console.log(chalk5.green(" \u2713 No changed files to push"));
|
|
18929
19459
|
process.exit(0);
|
|
18930
19460
|
}
|
|
18931
19461
|
await outputMultiFileResults(`pre-push (${files.length} changed files)`, files, opts);
|
|
@@ -18933,15 +19463,15 @@ function prePushSubcommand() {
|
|
|
18933
19463
|
}
|
|
18934
19464
|
function pathSubcommand() {
|
|
18935
19465
|
return addScanOpts(
|
|
18936
|
-
new
|
|
19466
|
+
new Command5("path").description("Scan a file or directory (recursive)").argument("<target>", "File or directory to scan")
|
|
18937
19467
|
).action(async (target, opts, cmd) => {
|
|
18938
19468
|
opts = inheritedScanOpts(cmd);
|
|
18939
19469
|
applyPolicy(opts);
|
|
18940
19470
|
const files = collectFiles(target);
|
|
18941
19471
|
if (files.length === 0) {
|
|
18942
19472
|
console.error(
|
|
18943
|
-
|
|
18944
|
-
x Path not found or empty: ${target}`) +
|
|
19473
|
+
chalk5.red(`
|
|
19474
|
+
x Path not found or empty: ${target}`) + chalk5.dim(`
|
|
18945
19475
|
Try: pretense scan path <file-or-directory>
|
|
18946
19476
|
`)
|
|
18947
19477
|
);
|
|
@@ -18951,7 +19481,7 @@ function pathSubcommand() {
|
|
|
18951
19481
|
});
|
|
18952
19482
|
}
|
|
18953
19483
|
function scanCommand() {
|
|
18954
|
-
const cmd = new
|
|
19484
|
+
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
19485
|
"--policy <preset>",
|
|
18956
19486
|
// A preset RE-WEIGHTS detector severity/action for that framework
|
|
18957
19487
|
// (see compliance-preset.ts `actionOverrides`). It does NOT narrow the
|
|
@@ -18974,8 +19504,8 @@ function scanCommand() {
|
|
|
18974
19504
|
const allFiles = collectFiles(file);
|
|
18975
19505
|
if (allFiles.length === 0) {
|
|
18976
19506
|
console.error(
|
|
18977
|
-
|
|
18978
|
-
No supported files found in ${file}.`) +
|
|
19507
|
+
chalk5.yellow(`
|
|
19508
|
+
No supported files found in ${file}.`) + chalk5.dim(`
|
|
18979
19509
|
Supported: ${SUPPORTED_EXTENSIONS_DISPLAY}
|
|
18980
19510
|
`)
|
|
18981
19511
|
);
|
|
@@ -18986,17 +19516,17 @@ function scanCommand() {
|
|
|
18986
19516
|
} catch {
|
|
18987
19517
|
}
|
|
18988
19518
|
if (isBinaryFile(file)) {
|
|
18989
|
-
console.error(
|
|
19519
|
+
console.error(chalk5.yellow(`
|
|
18990
19520
|
Skipping binary file: ${file}
|
|
18991
19521
|
`));
|
|
18992
19522
|
process.exit(0);
|
|
18993
19523
|
}
|
|
18994
19524
|
try {
|
|
18995
|
-
content =
|
|
19525
|
+
content = readFileSync8(file, "utf-8");
|
|
18996
19526
|
} catch {
|
|
18997
19527
|
console.error(
|
|
18998
|
-
|
|
18999
|
-
x Cannot read file: ${file}`) +
|
|
19528
|
+
chalk5.red(`
|
|
19529
|
+
x Cannot read file: ${file}`) + chalk5.dim(`
|
|
19000
19530
|
Try: check the file exists and you have read permission.
|
|
19001
19531
|
`)
|
|
19002
19532
|
);
|
|
@@ -19005,7 +19535,7 @@ function scanCommand() {
|
|
|
19005
19535
|
} else {
|
|
19006
19536
|
if (process.stdin.isTTY) {
|
|
19007
19537
|
console.error(
|
|
19008
|
-
|
|
19538
|
+
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
19539
|
);
|
|
19010
19540
|
process.exit(USAGE_EXIT);
|
|
19011
19541
|
}
|
|
@@ -19014,7 +19544,7 @@ function scanCommand() {
|
|
|
19014
19544
|
content = Buffer.concat(chunks).toString("utf-8");
|
|
19015
19545
|
if (content.trim() === "") {
|
|
19016
19546
|
console.error(
|
|
19017
|
-
|
|
19547
|
+
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"
|
|
19018
19548
|
);
|
|
19019
19549
|
process.exit(USAGE_EXIT);
|
|
19020
19550
|
}
|
|
@@ -19032,466 +19562,295 @@ function scanCommand() {
|
|
|
19032
19562
|
console.log(JSON.stringify(toSarif(fileResults, opts ?? {}), null, 2));
|
|
19033
19563
|
process.exit(result.clean ? 0 : 1);
|
|
19034
19564
|
}
|
|
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(
|
|
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);
|
|
19225
|
-
}
|
|
19226
|
-
const display = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
19227
|
-
console.log(display);
|
|
19228
|
-
});
|
|
19229
|
-
cmd.command("set").description("Set a config value").argument("<key>", "Config key (supports dotted paths)").argument("<value>", "Value to set").action((key, rawValue) => {
|
|
19230
|
-
const config = loadConfig2();
|
|
19231
|
-
const value = parseValue(rawValue);
|
|
19232
|
-
setNestedValue(config, key, value);
|
|
19233
|
-
const outPath = configFilePath();
|
|
19234
|
-
writeFileSync4(outPath, YAML2.stringify(config));
|
|
19235
|
-
console.log(chalk6.green(`
|
|
19236
|
-
\u2713 Set ${key} = ${JSON.stringify(value)} in ${outPath}
|
|
19565
|
+
if (fmt2 === "csv") {
|
|
19566
|
+
const fileResults = result.clean ? [] : [{ file: file ?? "stdin", result }];
|
|
19567
|
+
console.log(toCsv(fileResults, opts ?? {}));
|
|
19568
|
+
process.exit(result.clean ? 0 : 1);
|
|
19569
|
+
}
|
|
19570
|
+
console.log(chalk5.cyan(`
|
|
19571
|
+
Pretense Scan \u2014 ${file ?? "stdin"}
|
|
19237
19572
|
`));
|
|
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
|
|
19573
|
+
console.log(chalk5.dim(` Patterns: ${getPatternCount()} | Levels: ${levelsLabel(opts)} | Duration: ${result.durationMs}ms
|
|
19245
19574
|
`));
|
|
19246
|
-
|
|
19247
|
-
|
|
19575
|
+
{
|
|
19576
|
+
const policyLine = policyNotice(opts);
|
|
19577
|
+
if (policyLine) console.log(policyLine + "\n");
|
|
19578
|
+
}
|
|
19579
|
+
if (opts.level !== 1 && result.l2 && !result.l2.ran && result.l2.reason) {
|
|
19580
|
+
console.log(chalk5.yellow(` ! ${result.l2.reason}`));
|
|
19581
|
+
console.log(chalk5.dim(" L1 (regex) ran in full; no finding was dropped.\n"));
|
|
19582
|
+
}
|
|
19583
|
+
if (result.clean) {
|
|
19584
|
+
console.log(chalk5.green(" \u2713 No secrets or PII detected\n"));
|
|
19585
|
+
process.exit(0);
|
|
19586
|
+
}
|
|
19587
|
+
for (const match3 of result.matches) {
|
|
19588
|
+
const actionColor = match3.action === "block" ? chalk5.red : match3.action === "redact" ? chalk5.yellow : chalk5.dim;
|
|
19589
|
+
console.log(
|
|
19590
|
+
` ${severityIcon(match3.severity)} ${levelTag(match3.level)} ${chalk5.bold(match3.type.padEnd(30))} ${actionColor(match3.action.padEnd(8))} ${chalk5.dim(rowTail(match3, opts ?? {}))}`
|
|
19591
|
+
);
|
|
19248
19592
|
}
|
|
19593
|
+
const blocked = result.matches.filter((m) => m.action === "block").length;
|
|
19594
|
+
const redacted = result.matches.filter((m) => m.action === "redact").length;
|
|
19595
|
+
console.log(chalk5.dim(`
|
|
19596
|
+
Total: ${result.matches.length} findings | ${blocked} blocked, ${redacted} redacted
|
|
19597
|
+
`));
|
|
19598
|
+
process.exit(1);
|
|
19599
|
+
});
|
|
19600
|
+
cmd.addCommand(ciSubcommand());
|
|
19601
|
+
cmd.addCommand(commitRangeSubcommand());
|
|
19602
|
+
cmd.addCommand(preCommitSubcommand());
|
|
19603
|
+
cmd.addCommand(prePushSubcommand());
|
|
19604
|
+
cmd.addCommand(pathSubcommand());
|
|
19605
|
+
cmd.exitOverride((err) => {
|
|
19606
|
+
process.exit(err.exitCode === 0 ? 0 : 3);
|
|
19249
19607
|
});
|
|
19250
19608
|
return cmd;
|
|
19251
19609
|
}
|
|
19252
19610
|
|
|
19253
|
-
// src/commands/
|
|
19611
|
+
// src/commands/status.ts
|
|
19254
19612
|
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
|
-
|
|
19613
|
+
import { Command as Command6 } from "commander";
|
|
19614
|
+
import chalk6 from "chalk";
|
|
19615
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
19616
|
+
import { join as join9 } from "path";
|
|
19617
|
+
function statusCommand() {
|
|
19618
|
+
return new Command6("status").description("Show Pretense protection status").action(async () => {
|
|
19619
|
+
const dir = process.cwd();
|
|
19620
|
+
const fingerprintPath = join9(dir, ".pretense", "fingerprint.json");
|
|
19621
|
+
const configPath = join9(dir, "pretense.yaml");
|
|
19622
|
+
console.log(chalk6.cyan("\n Pretense Status\n"));
|
|
19623
|
+
const record = readProxyRecord(pidFilePath());
|
|
19624
|
+
if (record && !isProcessAlive(record.pid)) {
|
|
19625
|
+
console.log(
|
|
19626
|
+
chalk6.yellow(
|
|
19627
|
+
` \u25CB Proxy not running ${chalk6.dim(`(stale record for pid ${record.pid}; run: pretense start)`)}`
|
|
19628
|
+
)
|
|
19629
|
+
);
|
|
19630
|
+
} else {
|
|
19631
|
+
const port = record && record.port > 0 ? record.port : 9339;
|
|
19632
|
+
try {
|
|
19633
|
+
const resp = await fetch(`http://localhost:${port}/health`, { signal: AbortSignal.timeout(1e3) });
|
|
19634
|
+
if (!resp.ok) throw new Error(`health returned ${resp.status}`);
|
|
19635
|
+
const health = await resp.json();
|
|
19636
|
+
console.log(chalk6.green(` \u2713 Proxy running on :${port} (uptime: ${health["uptime"]}s)`));
|
|
19637
|
+
const stats = health["stats"];
|
|
19638
|
+
console.log(chalk6.dim(` Requests: ${stats["requestsProcessed"] ?? 0} | Mutations: ${stats["mutationsApplied"] ?? 0} | Blocked: ${stats["secretsBlocked"] ?? 0}`));
|
|
19639
|
+
} catch {
|
|
19640
|
+
console.log(chalk6.yellow(` \u25CB Proxy not running ${chalk6.dim("(run: pretense start)")}`));
|
|
19272
19641
|
}
|
|
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
19642
|
}
|
|
19277
|
-
if (
|
|
19278
|
-
console.log(
|
|
19279
|
-
|
|
19643
|
+
if (existsSync9(configPath)) {
|
|
19644
|
+
console.log(chalk6.green(` \u2713 Config: pretense.yaml`));
|
|
19645
|
+
} else {
|
|
19646
|
+
console.log(chalk6.yellow(` \u25CB No config ${chalk6.dim("(run: pretense config)")}`));
|
|
19280
19647
|
}
|
|
19281
|
-
if (
|
|
19282
|
-
|
|
19283
|
-
|
|
19648
|
+
if (existsSync9(fingerprintPath)) {
|
|
19649
|
+
try {
|
|
19650
|
+
const fp = JSON.parse(readFileSync9(fingerprintPath, "utf-8"));
|
|
19651
|
+
const age = Math.round((Date.now() - fp.scannedAt) / (60 * 1e3));
|
|
19652
|
+
console.log(chalk6.green(` \u2713 Fingerprint: ${fp.filesScanned} files, ${fp.functions.length + fp.classes.length + fp.constants.length} identifiers`));
|
|
19653
|
+
console.log(chalk6.dim(` Last scanned: ${age} minutes ago`));
|
|
19654
|
+
} catch {
|
|
19655
|
+
console.log(chalk6.yellow(" \u26A0 Fingerprint corrupted (run: pretense init)"));
|
|
19656
|
+
}
|
|
19657
|
+
} else {
|
|
19658
|
+
console.log(chalk6.yellow(` \u25CB No fingerprint ${chalk6.dim("(run: pretense init)")}`));
|
|
19284
19659
|
}
|
|
19285
|
-
console.log(
|
|
19286
|
-
|
|
19660
|
+
console.log(chalk6.dim(`
|
|
19661
|
+
Scanner: ${getPatternCount()} patterns active
|
|
19287
19662
|
`));
|
|
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
19663
|
});
|
|
19299
19664
|
}
|
|
19300
19665
|
|
|
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
|
|
19666
|
+
// src/commands/config.ts
|
|
19309
19667
|
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
|
-
|
|
19335
|
-
|
|
19336
|
-
|
|
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;
|
|
19428
|
-
try {
|
|
19429
|
-
store = new AuditStore(dbPath);
|
|
19430
|
-
const stats = store.getStats(sinceMs);
|
|
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
|
-
};
|
|
19668
|
+
import { Command as Command7 } from "commander";
|
|
19669
|
+
import chalk7 from "chalk";
|
|
19670
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
|
|
19671
|
+
import { join as join10 } from "path";
|
|
19672
|
+
import YAML2 from "yaml";
|
|
19673
|
+
var DEFAULT_CONFIG = {
|
|
19674
|
+
version: 1,
|
|
19675
|
+
languages: ["typescript", "python", "java", "go", "ruby", "rust"],
|
|
19676
|
+
exclude_paths: ["node_modules", ".git", "dist"],
|
|
19677
|
+
mutation_mode: "full",
|
|
19678
|
+
output_format: "interactive"
|
|
19679
|
+
};
|
|
19680
|
+
function findConfigPath() {
|
|
19681
|
+
const cwd = process.cwd();
|
|
19682
|
+
const candidates = [".pretense.yaml", "pretense.yaml"];
|
|
19683
|
+
for (const name of candidates) {
|
|
19684
|
+
const full = join10(cwd, name);
|
|
19685
|
+
if (existsSync10(full)) return full;
|
|
19686
|
+
}
|
|
19687
|
+
return null;
|
|
19688
|
+
}
|
|
19689
|
+
function loadConfig2() {
|
|
19690
|
+
const path2 = findConfigPath();
|
|
19691
|
+
if (!path2) return { ...DEFAULT_CONFIG };
|
|
19692
|
+
try {
|
|
19693
|
+
const raw2 = readFileSync10(path2, "utf-8");
|
|
19694
|
+
return YAML2.parse(raw2) ?? { ...DEFAULT_CONFIG };
|
|
19448
19695
|
} catch {
|
|
19449
|
-
return
|
|
19450
|
-
} finally {
|
|
19451
|
-
store?.close();
|
|
19696
|
+
return { ...DEFAULT_CONFIG };
|
|
19452
19697
|
}
|
|
19453
19698
|
}
|
|
19454
|
-
function
|
|
19455
|
-
return
|
|
19699
|
+
function configFilePath() {
|
|
19700
|
+
return findConfigPath() ?? join10(process.cwd(), "pretense.yaml");
|
|
19456
19701
|
}
|
|
19457
|
-
function
|
|
19458
|
-
const
|
|
19459
|
-
|
|
19460
|
-
|
|
19702
|
+
function getNestedValue(obj, key) {
|
|
19703
|
+
const parts = key.split(".");
|
|
19704
|
+
let cur = obj;
|
|
19705
|
+
for (const p of parts) {
|
|
19706
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
19707
|
+
cur = cur[p];
|
|
19708
|
+
}
|
|
19709
|
+
return cur;
|
|
19461
19710
|
}
|
|
19462
|
-
function
|
|
19463
|
-
|
|
19711
|
+
function setNestedValue(obj, key, value) {
|
|
19712
|
+
const parts = key.split(".");
|
|
19713
|
+
const leaf = parts.pop();
|
|
19714
|
+
if (leaf === void 0) return;
|
|
19715
|
+
let cur = obj;
|
|
19716
|
+
for (const p of parts) {
|
|
19717
|
+
const child = cur[p];
|
|
19718
|
+
if (child == null || typeof child !== "object" || Array.isArray(child)) {
|
|
19719
|
+
cur[p] = {};
|
|
19720
|
+
}
|
|
19721
|
+
cur = cur[p];
|
|
19722
|
+
}
|
|
19723
|
+
cur[leaf] = value;
|
|
19464
19724
|
}
|
|
19465
|
-
function
|
|
19466
|
-
if (
|
|
19467
|
-
|
|
19468
|
-
|
|
19725
|
+
function parseValue(raw2) {
|
|
19726
|
+
if (raw2.startsWith("[") && raw2.endsWith("]")) {
|
|
19727
|
+
return raw2.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
19728
|
+
}
|
|
19729
|
+
if (raw2 === "true") return true;
|
|
19730
|
+
if (raw2 === "false") return false;
|
|
19731
|
+
const n = Number(raw2);
|
|
19732
|
+
if (!Number.isNaN(n) && raw2.length > 0) return n;
|
|
19733
|
+
return raw2;
|
|
19469
19734
|
}
|
|
19470
|
-
function
|
|
19471
|
-
|
|
19472
|
-
|
|
19473
|
-
|
|
19474
|
-
|
|
19735
|
+
function configCommand() {
|
|
19736
|
+
const cmd = new Command7("config").description("Manage pretense configuration");
|
|
19737
|
+
cmd.command("list").description("Print current configuration").action(() => {
|
|
19738
|
+
const config = loadConfig2();
|
|
19739
|
+
const path2 = findConfigPath();
|
|
19740
|
+
console.log(
|
|
19741
|
+
chalk7.cyan(`
|
|
19742
|
+
Pretense Config`) + chalk7.dim(` (${path2 ?? "defaults - no config file found"})
|
|
19743
|
+
`)
|
|
19744
|
+
);
|
|
19745
|
+
console.log(YAML2.stringify(config).replace(/^/gm, " "));
|
|
19746
|
+
});
|
|
19747
|
+
cmd.command("get").description("Print a single config value").argument("<key>", "Config key (supports dotted paths)").action((key) => {
|
|
19748
|
+
const config = loadConfig2();
|
|
19749
|
+
const val = getNestedValue(config, key);
|
|
19750
|
+
if (val === void 0) {
|
|
19751
|
+
console.error(chalk7.red(`
|
|
19752
|
+
\u2717 Key not found: ${key}
|
|
19753
|
+
`));
|
|
19754
|
+
process.exit(1);
|
|
19755
|
+
}
|
|
19756
|
+
const display = typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
19757
|
+
console.log(display);
|
|
19758
|
+
});
|
|
19759
|
+
cmd.command("set").description("Set a config value").argument("<key>", "Config key (supports dotted paths)").argument("<value>", "Value to set").action((key, rawValue) => {
|
|
19760
|
+
const config = loadConfig2();
|
|
19761
|
+
const value = parseValue(rawValue);
|
|
19762
|
+
setNestedValue(config, key, value);
|
|
19763
|
+
const outPath = configFilePath();
|
|
19764
|
+
writeFileSync4(outPath, YAML2.stringify(config));
|
|
19765
|
+
console.log(chalk7.green(`
|
|
19766
|
+
\u2713 Set ${key} = ${JSON.stringify(value)} in ${outPath}
|
|
19767
|
+
`));
|
|
19768
|
+
});
|
|
19769
|
+
cmd.option("--force", "Overwrite existing config with defaults", false).action((opts) => {
|
|
19770
|
+
if (opts.force) {
|
|
19771
|
+
const outPath = join10(process.cwd(), "pretense.yaml");
|
|
19772
|
+
writeFileSync4(outPath, YAML2.stringify(DEFAULT_CONFIG));
|
|
19773
|
+
console.log(chalk7.green(`
|
|
19774
|
+
\u2713 Created pretense.yaml
|
|
19775
|
+
`));
|
|
19776
|
+
} else {
|
|
19777
|
+
cmd.help();
|
|
19778
|
+
}
|
|
19779
|
+
});
|
|
19780
|
+
return cmd;
|
|
19781
|
+
}
|
|
19782
|
+
|
|
19783
|
+
// src/commands/logs.ts
|
|
19784
|
+
init_esm_shims();
|
|
19785
|
+
import { Command as Command8 } from "commander";
|
|
19786
|
+
import chalk8 from "chalk";
|
|
19787
|
+
import { homedir as homedir5 } from "os";
|
|
19788
|
+
import { join as join11 } from "path";
|
|
19789
|
+
function logsCommand() {
|
|
19790
|
+
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) => {
|
|
19791
|
+
const limit = parseInt(opts.limit, 10);
|
|
19792
|
+
let store;
|
|
19793
|
+
let entries;
|
|
19794
|
+
try {
|
|
19795
|
+
store = new AuditStore(join11(homedir5(), ".pretense", "audit.db"));
|
|
19796
|
+
entries = store.getRecentEntries(limit);
|
|
19797
|
+
store.close();
|
|
19798
|
+
} catch {
|
|
19799
|
+
if (opts.json) {
|
|
19800
|
+
console.log("[]");
|
|
19801
|
+
return;
|
|
19802
|
+
}
|
|
19803
|
+
console.error(chalk8.dim("\n No audit data available. The audit database has not been initialized yet."));
|
|
19804
|
+
console.error(chalk8.dim(" Run " + chalk8.cyan("pretense start") + " and make some requests first.\n"));
|
|
19805
|
+
return;
|
|
19806
|
+
}
|
|
19807
|
+
if (opts.json) {
|
|
19808
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
19809
|
+
return;
|
|
19810
|
+
}
|
|
19811
|
+
if (entries.length === 0) {
|
|
19812
|
+
console.log(chalk8.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
|
|
19813
|
+
return;
|
|
19814
|
+
}
|
|
19815
|
+
console.log(chalk8.cyan(`
|
|
19816
|
+
Pretense Audit Log \u2014 last ${entries.length} entries
|
|
19817
|
+
`));
|
|
19818
|
+
console.log(chalk8.dim(" " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATIONS".padEnd(12) + "BLOCKED".padEnd(10) + "MS"));
|
|
19819
|
+
console.log(chalk8.dim(" " + "\u2500".repeat(70)));
|
|
19820
|
+
for (const entry of entries) {
|
|
19821
|
+
const time = new Date(entry.timestamp).toTimeString().slice(0, 8);
|
|
19822
|
+
const actionColor = entry.action === "blocked" ? chalk8.red : entry.action === "mutated" ? chalk8.green : entry.action === "redacted" ? chalk8.yellow : entry.action === "error" ? chalk8.red : chalk8.dim;
|
|
19823
|
+
console.log(
|
|
19824
|
+
" " + 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))
|
|
19825
|
+
);
|
|
19826
|
+
}
|
|
19827
|
+
console.log();
|
|
19828
|
+
});
|
|
19475
19829
|
}
|
|
19476
19830
|
|
|
19477
19831
|
// src/commands/usage.ts
|
|
19832
|
+
init_esm_shims();
|
|
19833
|
+
import { Command as Command9 } from "commander";
|
|
19834
|
+
import chalk9 from "chalk";
|
|
19835
|
+
import { join as join12 } from "path";
|
|
19836
|
+
import { homedir as homedir6 } from "os";
|
|
19478
19837
|
function renderBar(percent, width = 30) {
|
|
19479
19838
|
const filled = Math.min(Math.round(percent / 100 * width), width);
|
|
19480
19839
|
const empty = width - filled;
|
|
19481
19840
|
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
|
|
19841
|
+
if (percent >= 100) return chalk9.red(bar2);
|
|
19842
|
+
if (percent >= 90) return chalk9.yellow(bar2);
|
|
19843
|
+
if (percent >= 80) return chalk9.yellow(bar2);
|
|
19844
|
+
return chalk9.green(bar2);
|
|
19486
19845
|
}
|
|
19487
19846
|
function formatMetric(value, limit) {
|
|
19488
19847
|
if (limit === null) return `${value.toLocaleString("en-US")} / unlimited`;
|
|
19489
19848
|
return `${value.toLocaleString("en-US")} / ${limit.toLocaleString("en-US")}`;
|
|
19490
19849
|
}
|
|
19491
19850
|
function percentLabel(percent) {
|
|
19492
|
-
if (percent >= 100) return
|
|
19493
|
-
if (percent >= 80) return
|
|
19494
|
-
return
|
|
19851
|
+
if (percent >= 100) return chalk9.red("OVER LIMIT");
|
|
19852
|
+
if (percent >= 80) return chalk9.yellow(`${percent}%`);
|
|
19853
|
+
return chalk9.green(`${percent}%`);
|
|
19495
19854
|
}
|
|
19496
19855
|
function emptyUsagePayload(resolution) {
|
|
19497
19856
|
const tier = resolution.tier;
|
|
@@ -19513,7 +19872,7 @@ function emptyUsagePayload(resolution) {
|
|
|
19513
19872
|
};
|
|
19514
19873
|
}
|
|
19515
19874
|
function usageCommand() {
|
|
19516
|
-
return new
|
|
19875
|
+
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
19876
|
const resolution = await resolvePlanTier({ offline: opts.offline });
|
|
19518
19877
|
const tier = resolution.tier;
|
|
19519
19878
|
const collected = collectUsage(opts.db);
|
|
@@ -19524,7 +19883,7 @@ function usageCommand() {
|
|
|
19524
19883
|
return;
|
|
19525
19884
|
}
|
|
19526
19885
|
console.log(
|
|
19527
|
-
|
|
19886
|
+
chalk9.yellow("\n No audit data found. Run ") + chalk9.cyan("pretense start") + chalk9.yellow(" and make some requests first.\n")
|
|
19528
19887
|
);
|
|
19529
19888
|
return;
|
|
19530
19889
|
}
|
|
@@ -19567,7 +19926,7 @@ function usageCommand() {
|
|
|
19567
19926
|
}
|
|
19568
19927
|
const resetDate = resetCadenceLabel();
|
|
19569
19928
|
console.log(
|
|
19570
|
-
|
|
19929
|
+
chalk9.cyan(
|
|
19571
19930
|
"\n Pretense Usage \u2014 " + // Header shows today's date, NOT a billing month: quotas roll on a
|
|
19572
19931
|
// 7-day window anchored to signup, so a month name would imply a
|
|
19573
19932
|
// reset boundary that does not exist.
|
|
@@ -19578,11 +19937,14 @@ function usageCommand() {
|
|
|
19578
19937
|
})
|
|
19579
19938
|
)
|
|
19580
19939
|
);
|
|
19581
|
-
console.log(
|
|
19940
|
+
console.log(chalk9.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetDate}
|
|
19582
19941
|
`));
|
|
19942
|
+
for (const line of planMismatchHint(resolution)) {
|
|
19943
|
+
console.log(chalk9.dim(` ${line}`));
|
|
19944
|
+
}
|
|
19583
19945
|
if (!tier) {
|
|
19584
19946
|
for (const line of unverifiedNote(resolution.detail)) {
|
|
19585
|
-
console.log(
|
|
19947
|
+
console.log(chalk9.yellow(` ${line}`));
|
|
19586
19948
|
}
|
|
19587
19949
|
console.log();
|
|
19588
19950
|
console.log(" " + "Mutations".padEnd(20) + usageMetrics.mutationsApplied.toLocaleString("en-US"));
|
|
@@ -19595,42 +19957,42 @@ function usageCommand() {
|
|
|
19595
19957
|
const overall = maxUsagePercent(usageMetrics, tier);
|
|
19596
19958
|
const nextTier = recommendedTier(usageMetrics, tier);
|
|
19597
19959
|
console.log(
|
|
19598
|
-
" " +
|
|
19960
|
+
" " + chalk9.bold("Metric".padEnd(20)) + chalk9.bold("Used / Limit".padEnd(30)) + chalk9.bold("Progress")
|
|
19599
19961
|
);
|
|
19600
|
-
console.log(" " +
|
|
19962
|
+
console.log(" " + chalk9.dim("\u2500".repeat(72)));
|
|
19601
19963
|
const rows = [
|
|
19602
19964
|
{ label: "Mutations", value: usageMetrics.mutationsApplied, limit: planConfig.mutationsPerPeriod },
|
|
19603
19965
|
{ label: "API Requests", value: usageMetrics.totalScans, limit: planConfig.scansPerPeriod }
|
|
19604
19966
|
];
|
|
19605
19967
|
for (const row of rows) {
|
|
19606
19968
|
const percent = row.limit === null ? 0 : Math.round(row.value / row.limit * 100);
|
|
19607
|
-
const bar2 = row.limit === null ?
|
|
19969
|
+
const bar2 = row.limit === null ? chalk9.dim(" unlimited ".padEnd(32)) : renderBar(percent);
|
|
19608
19970
|
console.log(
|
|
19609
19971
|
" " + row.label.padEnd(20) + formatMetric(row.value, row.limit).padEnd(30) + bar2 + " " + (row.limit === null ? "" : percentLabel(percent))
|
|
19610
19972
|
);
|
|
19611
19973
|
}
|
|
19612
19974
|
console.log(
|
|
19613
|
-
" " + "Secrets Blocked".padEnd(20) + String(usageMetrics.secretsBlocked.toLocaleString("en-US")).padEnd(30) +
|
|
19975
|
+
" " + "Secrets Blocked".padEnd(20) + String(usageMetrics.secretsBlocked.toLocaleString("en-US")).padEnd(30) + chalk9.dim("(no limit)")
|
|
19614
19976
|
);
|
|
19615
|
-
console.log(" " +
|
|
19977
|
+
console.log(" " + chalk9.dim("\u2500".repeat(72)));
|
|
19616
19978
|
if (tier === "enterprise") {
|
|
19617
|
-
console.log(
|
|
19979
|
+
console.log(chalk9.green("\n Enterprise plan \u2014 all limits are unlimited.\n"));
|
|
19618
19980
|
} else if (overall >= 100) {
|
|
19619
|
-
console.log(
|
|
19981
|
+
console.log(chalk9.red(`
|
|
19620
19982
|
LIMIT REACHED (${overall}% of ${tierLabel(tier)} plan used).`));
|
|
19621
19983
|
} else if (overall >= 90) {
|
|
19622
19984
|
console.log(
|
|
19623
|
-
|
|
19985
|
+
chalk9.yellow(
|
|
19624
19986
|
`
|
|
19625
19987
|
WARNING: ${overall}% of your ${tierLabel(tier)} plan used \u2014 approaching limit.`
|
|
19626
19988
|
)
|
|
19627
19989
|
);
|
|
19628
19990
|
} else if (overall >= 80) {
|
|
19629
|
-
console.log(
|
|
19991
|
+
console.log(chalk9.yellow(`
|
|
19630
19992
|
NOTICE: ${overall}% of your ${tierLabel(tier)} plan used.`));
|
|
19631
19993
|
} else {
|
|
19632
19994
|
console.log(
|
|
19633
|
-
|
|
19995
|
+
chalk9.green(`
|
|
19634
19996
|
${overall}% of your ${tierLabel(tier)} plan used \u2014 you're in good shape.`)
|
|
19635
19997
|
);
|
|
19636
19998
|
}
|
|
@@ -19642,12 +20004,12 @@ function usageCommand() {
|
|
|
19642
20004
|
enterprise: "Unlimited mutations + SSO + on-prem + SIEM"
|
|
19643
20005
|
};
|
|
19644
20006
|
console.log(
|
|
19645
|
-
|
|
20007
|
+
chalk9.cyan(`
|
|
19646
20008
|
Upgrade to ${tierName(nextTier)} (${priceLabel(nextTier)}):`)
|
|
19647
20009
|
);
|
|
19648
|
-
console.log(
|
|
20010
|
+
console.log(chalk9.dim(` ${upgradeBenefit[nextTier]}`));
|
|
19649
20011
|
console.log(
|
|
19650
|
-
|
|
20012
|
+
chalk9.bold(`
|
|
19651
20013
|
\u2192 https://pretense.ai/pricing?ref=cli_usage&from=${tier}
|
|
19652
20014
|
`)
|
|
19653
20015
|
);
|
|
@@ -19659,12 +20021,12 @@ function usageCommand() {
|
|
|
19659
20021
|
|
|
19660
20022
|
// src/commands/auth.ts
|
|
19661
20023
|
init_esm_shims();
|
|
19662
|
-
import { Command as
|
|
19663
|
-
import
|
|
20024
|
+
import { Command as Command10 } from "commander";
|
|
20025
|
+
import chalk10 from "chalk";
|
|
19664
20026
|
import { existsSync as existsSync11, readFileSync as readFileSync11, unlinkSync as unlinkSync2 } from "fs";
|
|
19665
20027
|
import { join as join13 } from "path";
|
|
19666
20028
|
import { homedir as homedir7 } from "os";
|
|
19667
|
-
import { execSync as
|
|
20029
|
+
import { execSync as execSync4 } from "child_process";
|
|
19668
20030
|
var CREDENTIALS_DIR = join13(homedir7(), ".pretense");
|
|
19669
20031
|
var CREDENTIALS_PATH = join13(CREDENTIALS_DIR, "credentials.json");
|
|
19670
20032
|
async function verifyApiKey(apiKey) {
|
|
@@ -19708,7 +20070,7 @@ async function verifyApiKey(apiKey) {
|
|
|
19708
20070
|
}
|
|
19709
20071
|
}
|
|
19710
20072
|
function storageNotice() {
|
|
19711
|
-
return
|
|
20073
|
+
return chalk10.dim(` Stored: ${CREDENTIALS_PATH}`) + chalk10.dim(`
|
|
19712
20074
|
Mode: 0600 (file), 0700 (~/.pretense) \u2014 plaintext, not encrypted
|
|
19713
20075
|
`);
|
|
19714
20076
|
}
|
|
@@ -19731,15 +20093,15 @@ function maskApiKey(key) {
|
|
|
19731
20093
|
function openUrl(url) {
|
|
19732
20094
|
try {
|
|
19733
20095
|
if (process.platform === "darwin") {
|
|
19734
|
-
|
|
20096
|
+
execSync4(`open ${url}`, { stdio: "ignore" });
|
|
19735
20097
|
} else {
|
|
19736
|
-
|
|
20098
|
+
execSync4(`xdg-open ${url}`, { stdio: "ignore" });
|
|
19737
20099
|
}
|
|
19738
20100
|
} catch {
|
|
19739
20101
|
}
|
|
19740
20102
|
}
|
|
19741
20103
|
function authCommand() {
|
|
19742
|
-
const auth = new
|
|
20104
|
+
const auth = new Command10("auth").description("Manage CLI authentication");
|
|
19743
20105
|
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
20106
|
"--offline",
|
|
19745
20107
|
"Skip server verification and store the key unverified (says so in the output)",
|
|
@@ -19758,9 +20120,9 @@ function authCommand() {
|
|
|
19758
20120
|
const isValidFormat = validPrefixes.some((prefix) => opts.apiKey.startsWith(prefix));
|
|
19759
20121
|
if (!isValidFormat) {
|
|
19760
20122
|
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`) +
|
|
20123
|
+
chalk10.red(`
|
|
20124
|
+
x Invalid API key format.`) + chalk10.dim(`
|
|
20125
|
+
Key must start with prtns_live_, prtns_test_, or a legacy pt-free-/pt-pro-/pt-ent- prefix`) + chalk10.dim(`
|
|
19764
20126
|
Try: pretense auth login (opens browser to get a valid key)
|
|
19765
20127
|
`)
|
|
19766
20128
|
);
|
|
@@ -19775,10 +20137,10 @@ function authCommand() {
|
|
|
19775
20137
|
const existing = readCredentials();
|
|
19776
20138
|
if (existing?.verified === true) {
|
|
19777
20139
|
console.error(
|
|
19778
|
-
|
|
19779
|
-
Rejected: ${maskApiKey(opts.apiKey)}`) +
|
|
19780
|
-
Kept: ${maskApiKey(existing.api_key)} (verified ${existing.verified_at ?? "?"})`) +
|
|
19781
|
-
Server: ${verification.url}`) +
|
|
20140
|
+
chalk10.red("\n x The API key was rejected \u2014 your existing key was kept.") + chalk10.dim(`
|
|
20141
|
+
Rejected: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
|
|
20142
|
+
Kept: ${maskApiKey(existing.api_key)} (verified ${existing.verified_at ?? "?"})`) + chalk10.dim(`
|
|
20143
|
+
Server: ${verification.url}`) + chalk10.dim(`
|
|
19782
20144
|
Reason: ${verification.detail}
|
|
19783
20145
|
`)
|
|
19784
20146
|
);
|
|
@@ -19793,10 +20155,10 @@ function authCommand() {
|
|
|
19793
20155
|
rejected_by: verification.url
|
|
19794
20156
|
});
|
|
19795
20157
|
console.error(
|
|
19796
|
-
|
|
19797
|
-
Key: ${maskApiKey(opts.apiKey)}`) +
|
|
19798
|
-
Server: ${verification.url}`) +
|
|
19799
|
-
Reason: ${verification.detail}`) +
|
|
20158
|
+
chalk10.red("\n x The Pretense API REJECTED this key. It will not work.") + chalk10.dim(`
|
|
20159
|
+
Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
|
|
20160
|
+
Server: ${verification.url}`) + chalk10.dim(`
|
|
20161
|
+
Reason: ${verification.detail}`) + chalk10.dim(`
|
|
19800
20162
|
Get a current key at https://pretense.ai/auth
|
|
19801
20163
|
`) + storageNotice()
|
|
19802
20164
|
);
|
|
@@ -19814,19 +20176,22 @@ function authCommand() {
|
|
|
19814
20176
|
};
|
|
19815
20177
|
writeCredentials(creds);
|
|
19816
20178
|
if (verified) {
|
|
20179
|
+
const planIsFree = String(verification.plan ?? "").trim().toLowerCase() === "free";
|
|
19817
20180
|
console.log(
|
|
19818
|
-
|
|
19819
|
-
Key: ${maskApiKey(opts.apiKey)}`) +
|
|
19820
|
-
Server: ${verification.url}`) + (verification.plan ?
|
|
19821
|
-
Plan: ${verification.plan}`) : "") +
|
|
20181
|
+
chalk10.green("\n Authenticated. The key was verified by the Pretense API.") + chalk10.dim(`
|
|
20182
|
+
Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
|
|
20183
|
+
Server: ${verification.url}`) + (verification.plan ? chalk10.dim(`
|
|
20184
|
+
Plan: ${verification.plan}`) : "") + (planIsFree ? chalk10.dim(
|
|
20185
|
+
"\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."
|
|
20186
|
+
) : "") + "\n" + storageNotice()
|
|
19822
20187
|
);
|
|
19823
20188
|
return;
|
|
19824
20189
|
}
|
|
19825
20190
|
console.log(
|
|
19826
|
-
|
|
19827
|
-
Key: ${maskApiKey(opts.apiKey)}`) +
|
|
19828
|
-
Server: ${verification.url}`) +
|
|
19829
|
-
Reason: ${verification.detail}`) +
|
|
20191
|
+
chalk10.yellow("\n Key saved \u2014 NOT verified.") + chalk10.dim(`
|
|
20192
|
+
Key: ${maskApiKey(opts.apiKey)}`) + chalk10.dim(`
|
|
20193
|
+
Server: ${verification.url}`) + chalk10.dim(`
|
|
20194
|
+
Reason: ${verification.detail}`) + chalk10.dim(
|
|
19830
20195
|
`
|
|
19831
20196
|
The key's format is valid; nothing has confirmed the key itself.
|
|
19832
20197
|
Re-run \`pretense auth login --key <key>\` when online to verify it.
|
|
@@ -19836,58 +20201,58 @@ function authCommand() {
|
|
|
19836
20201
|
return;
|
|
19837
20202
|
}
|
|
19838
20203
|
const authUrl = "https://pretense.ai/auth";
|
|
19839
|
-
console.log(
|
|
19840
|
-
console.log(
|
|
19841
|
-
console.log(
|
|
19842
|
-
console.log(
|
|
19843
|
-
console.log(
|
|
20204
|
+
console.log(chalk10.cyan("\n Opening browser to https://pretense.ai/auth"));
|
|
20205
|
+
console.log(chalk10.white(" 1. Sign in or create your account"));
|
|
20206
|
+
console.log(chalk10.white(" 2. Copy your API key from the page"));
|
|
20207
|
+
console.log(chalk10.white(" 3. Return here and run:"));
|
|
20208
|
+
console.log(chalk10.green(" pretense auth login --api-key <your-api-key>\n"));
|
|
19844
20209
|
openUrl(authUrl);
|
|
19845
20210
|
});
|
|
19846
20211
|
auth.command("logout").description("Remove stored credentials").action(() => {
|
|
19847
20212
|
if (!existsSync11(CREDENTIALS_PATH)) {
|
|
19848
|
-
console.log(
|
|
20213
|
+
console.log(chalk10.yellow("\n No credentials found. Already logged out.\n"));
|
|
19849
20214
|
return;
|
|
19850
20215
|
}
|
|
19851
20216
|
unlinkSync2(CREDENTIALS_PATH);
|
|
19852
|
-
console.log(
|
|
20217
|
+
console.log(chalk10.green("\n Logged out. Credentials removed.\n"));
|
|
19853
20218
|
});
|
|
19854
20219
|
auth.command("status").description("Show current authentication state").action(() => {
|
|
19855
20220
|
const creds = readCredentials();
|
|
19856
20221
|
if (!creds) {
|
|
19857
|
-
console.log(
|
|
20222
|
+
console.log(chalk10.yellow("\n Not authenticated."));
|
|
19858
20223
|
console.log(
|
|
19859
|
-
|
|
20224
|
+
chalk10.dim(" Run ") + chalk10.cyan("pretense auth login --api-key <key>") + chalk10.dim(" to authenticate.\n")
|
|
19860
20225
|
);
|
|
19861
20226
|
return;
|
|
19862
20227
|
}
|
|
19863
20228
|
if (creds.verified === true) {
|
|
19864
|
-
console.log(
|
|
20229
|
+
console.log(chalk10.green("\n Authenticated (key verified by the Pretense API)"));
|
|
19865
20230
|
} else if (creds.rejected_at) {
|
|
19866
|
-
console.log(
|
|
20231
|
+
console.log(chalk10.red("\n Key stored \u2014 REJECTED by the Pretense API"));
|
|
19867
20232
|
console.log(
|
|
19868
|
-
|
|
20233
|
+
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
20234
|
);
|
|
19870
20235
|
} else {
|
|
19871
|
-
console.log(
|
|
20236
|
+
console.log(chalk10.yellow("\n Key stored \u2014 NOT verified"));
|
|
19872
20237
|
console.log(
|
|
19873
|
-
|
|
20238
|
+
chalk10.dim(" No server has confirmed this key. Run ") + chalk10.cyan("pretense auth login --key <key>") + chalk10.dim(" while online to verify it.")
|
|
19874
20239
|
);
|
|
19875
20240
|
}
|
|
19876
|
-
console.log(
|
|
20241
|
+
console.log(chalk10.dim(` API Key: ${maskApiKey(creds.api_key)}`));
|
|
19877
20242
|
if (creds.authenticated_at) {
|
|
19878
|
-
console.log(
|
|
20243
|
+
console.log(chalk10.dim(` Saved: ${creds.authenticated_at}`));
|
|
19879
20244
|
}
|
|
19880
20245
|
if (creds.verified_at) {
|
|
19881
|
-
console.log(
|
|
20246
|
+
console.log(chalk10.dim(` Verified: ${creds.verified_at} by ${creds.verified_by ?? "?"}`));
|
|
19882
20247
|
}
|
|
19883
20248
|
if (creds.plan) {
|
|
19884
|
-
console.log(
|
|
20249
|
+
console.log(chalk10.dim(` Plan: ${creds.plan}`));
|
|
19885
20250
|
}
|
|
19886
|
-
console.log(
|
|
20251
|
+
console.log(chalk10.dim(` Stored: ${CREDENTIALS_PATH} (mode 0600, plaintext)`));
|
|
19887
20252
|
if (creds.expires_at) {
|
|
19888
20253
|
const expired = new Date(creds.expires_at) < /* @__PURE__ */ new Date();
|
|
19889
|
-
const label = expired ?
|
|
19890
|
-
console.log(
|
|
20254
|
+
const label = expired ? chalk10.red("Expired") : chalk10.green("Valid");
|
|
20255
|
+
console.log(chalk10.dim(` Expires: ${creds.expires_at} (${label})`));
|
|
19891
20256
|
}
|
|
19892
20257
|
console.log();
|
|
19893
20258
|
});
|
|
@@ -19896,8 +20261,8 @@ function authCommand() {
|
|
|
19896
20261
|
|
|
19897
20262
|
// src/commands/review.ts
|
|
19898
20263
|
init_esm_shims();
|
|
19899
|
-
import { Command as
|
|
19900
|
-
import
|
|
20264
|
+
import { Command as Command11 } from "commander";
|
|
20265
|
+
import chalk11 from "chalk";
|
|
19901
20266
|
import { readdirSync as readdirSync3, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
|
|
19902
20267
|
import { join as join14, extname as extname3, relative, isAbsolute } from "path";
|
|
19903
20268
|
var SUPPORTED_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
@@ -20072,37 +20437,37 @@ function formatInteractive(results) {
|
|
|
20072
20437
|
const filesWithFindings = results.filter((r) => r.mutations.length > 0 || r.secrets.length > 0);
|
|
20073
20438
|
const totalMutations = results.reduce((sum, r) => sum + r.mutations.length, 0);
|
|
20074
20439
|
const totalSecrets = results.reduce((sum, r) => sum + r.secrets.length, 0);
|
|
20075
|
-
console.log(
|
|
20076
|
-
console.log(
|
|
20440
|
+
console.log(chalk11.cyan("\n Pretense Review\n"));
|
|
20441
|
+
console.log(chalk11.dim(` Files scanned: ${results.length} | With findings: ${filesWithFindings.length}
|
|
20077
20442
|
`));
|
|
20078
20443
|
if (totalMutations === 0 && totalSecrets === 0) {
|
|
20079
|
-
console.log(
|
|
20444
|
+
console.log(chalk11.green(" No mutations or secrets detected.\n"));
|
|
20080
20445
|
return;
|
|
20081
20446
|
}
|
|
20082
|
-
console.log(
|
|
20447
|
+
console.log(chalk11.bold(` ${totalMutations} mutation(s) | ${totalSecrets} secret(s)
|
|
20083
20448
|
`));
|
|
20084
20449
|
for (const r of filesWithFindings) {
|
|
20085
|
-
console.log(
|
|
20450
|
+
console.log(chalk11.bold(` ${r.file}`));
|
|
20086
20451
|
for (const m of r.mutations) {
|
|
20087
|
-
const catColor = m.category === "function" ?
|
|
20088
|
-
console.log(` ${catColor(m.category.padEnd(10))} ${
|
|
20452
|
+
const catColor = m.category === "function" ? chalk11.blue : m.category === "class" ? chalk11.magenta : m.category === "constant" ? chalk11.yellow : chalk11.white;
|
|
20453
|
+
console.log(` ${catColor(m.category.padEnd(10))} ${chalk11.red(m.original)} ${chalk11.dim("->")} ${chalk11.green(m.mutated)} ${chalk11.dim(`L${m.line}`)}`);
|
|
20089
20454
|
}
|
|
20090
20455
|
for (const s of r.secrets) {
|
|
20091
|
-
const icon = s.severity === "critical" ?
|
|
20092
|
-
console.log(` ${icon.padEnd(10)} ${
|
|
20456
|
+
const icon = s.severity === "critical" ? chalk11.red("CRIT") : s.severity === "high" ? chalk11.yellow("HIGH") : chalk11.dim(s.severity.toUpperCase());
|
|
20457
|
+
console.log(` ${icon.padEnd(10)} ${chalk11.bold(s.type)} ${chalk11.dim(s.locator)}`);
|
|
20093
20458
|
}
|
|
20094
20459
|
console.log();
|
|
20095
20460
|
}
|
|
20096
20461
|
}
|
|
20097
20462
|
function reviewCommand() {
|
|
20098
|
-
return new
|
|
20463
|
+
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
20464
|
const targetDir = path2 ? isAbsolute(path2) ? path2 : join14(process.cwd(), path2) : process.cwd();
|
|
20100
20465
|
const files = discoverFiles(targetDir);
|
|
20101
20466
|
if (files.length === 0) {
|
|
20102
20467
|
if (opts?.json || opts?.agent) {
|
|
20103
20468
|
console.log("[]");
|
|
20104
20469
|
} else {
|
|
20105
|
-
console.error(
|
|
20470
|
+
console.error(chalk11.yellow(" No supported source files found.\n"));
|
|
20106
20471
|
}
|
|
20107
20472
|
return;
|
|
20108
20473
|
}
|
|
@@ -20129,8 +20494,8 @@ function reviewCommand() {
|
|
|
20129
20494
|
|
|
20130
20495
|
// src/commands/install.ts
|
|
20131
20496
|
init_esm_shims();
|
|
20132
|
-
import { Command as
|
|
20133
|
-
import
|
|
20497
|
+
import { Command as Command12 } from "commander";
|
|
20498
|
+
import chalk12 from "chalk";
|
|
20134
20499
|
import {
|
|
20135
20500
|
existsSync as existsSync12,
|
|
20136
20501
|
writeFileSync as writeFileSync5,
|
|
@@ -20138,7 +20503,7 @@ import {
|
|
|
20138
20503
|
readFileSync as readFileSync13,
|
|
20139
20504
|
mkdirSync as mkdirSync3
|
|
20140
20505
|
} from "fs";
|
|
20141
|
-
import { join as join15, resolve as
|
|
20506
|
+
import { join as join15, resolve as resolve4 } from "path";
|
|
20142
20507
|
var HOOK_SCRIPTS = {
|
|
20143
20508
|
"pre-commit": `#!/bin/sh
|
|
20144
20509
|
# Pretense AI Firewall \u2014 pre-commit hook
|
|
@@ -20176,16 +20541,16 @@ function installHook(hooksDir, mode, force) {
|
|
|
20176
20541
|
return { installed: true, path: hookPath };
|
|
20177
20542
|
}
|
|
20178
20543
|
function installCommand() {
|
|
20179
|
-
return new
|
|
20544
|
+
return new Command12("install").description("Install Pretense git hooks (pre-commit and/or pre-push)").option(
|
|
20180
20545
|
"-m, --mode <mode>",
|
|
20181
20546
|
"Hook mode: pre-commit, pre-push (omit to install both)"
|
|
20182
20547
|
).option("-f, --force", "Overwrite existing hooks without prompting", false).option("-d, --dir <path>", "Project directory (defaults to cwd)", ".").action((opts) => {
|
|
20183
|
-
const dir =
|
|
20548
|
+
const dir = resolve4(opts.dir);
|
|
20184
20549
|
const force = opts.force;
|
|
20185
20550
|
const modeArg = opts.mode;
|
|
20186
20551
|
if (modeArg && !VALID_MODES.includes(modeArg)) {
|
|
20187
20552
|
console.error(
|
|
20188
|
-
|
|
20553
|
+
chalk12.red(
|
|
20189
20554
|
`
|
|
20190
20555
|
Error: Invalid mode "${modeArg}". Use: ${VALID_MODES.join(", ")}
|
|
20191
20556
|
`
|
|
@@ -20197,7 +20562,7 @@ function installCommand() {
|
|
|
20197
20562
|
const gitDir = findGitDir(dir);
|
|
20198
20563
|
if (!gitDir) {
|
|
20199
20564
|
console.error(
|
|
20200
|
-
|
|
20565
|
+
chalk12.red(
|
|
20201
20566
|
`
|
|
20202
20567
|
Error: No .git directory found in ${dir}
|
|
20203
20568
|
Run this command from a git repository root.
|
|
@@ -20208,23 +20573,23 @@ function installCommand() {
|
|
|
20208
20573
|
}
|
|
20209
20574
|
const hooksDir = join15(gitDir, "hooks");
|
|
20210
20575
|
mkdirSync3(hooksDir, { recursive: true });
|
|
20211
|
-
console.log(
|
|
20576
|
+
console.log(chalk12.cyan("\n Pretense hook installer\n"));
|
|
20212
20577
|
let hasError = false;
|
|
20213
20578
|
for (const mode of modes) {
|
|
20214
20579
|
const result = installHook(hooksDir, mode, force);
|
|
20215
20580
|
if (result.installed) {
|
|
20216
20581
|
console.log(
|
|
20217
|
-
|
|
20582
|
+
chalk12.green(` Installed ${mode} hook at ${result.path}`)
|
|
20218
20583
|
);
|
|
20219
20584
|
} else if (result.skipped) {
|
|
20220
20585
|
console.log(
|
|
20221
|
-
|
|
20586
|
+
chalk12.dim(
|
|
20222
20587
|
` Pretense ${mode} hook already installed at ${result.path}`
|
|
20223
20588
|
)
|
|
20224
20589
|
);
|
|
20225
20590
|
} else {
|
|
20226
20591
|
console.error(
|
|
20227
|
-
|
|
20592
|
+
chalk12.yellow(
|
|
20228
20593
|
` Hook already exists at ${result.path}. Use --force to overwrite.`
|
|
20229
20594
|
)
|
|
20230
20595
|
);
|
|
@@ -20235,14 +20600,14 @@ function installCommand() {
|
|
|
20235
20600
|
console.log("");
|
|
20236
20601
|
process.exit(1);
|
|
20237
20602
|
}
|
|
20238
|
-
console.log(
|
|
20603
|
+
console.log(chalk12.bold.green("\n Git hooks installed successfully.\n"));
|
|
20239
20604
|
});
|
|
20240
20605
|
}
|
|
20241
20606
|
|
|
20242
20607
|
// src/commands/ignore.ts
|
|
20243
20608
|
init_esm_shims();
|
|
20244
|
-
import { Command as
|
|
20245
|
-
import
|
|
20609
|
+
import { Command as Command13 } from "commander";
|
|
20610
|
+
import chalk13 from "chalk";
|
|
20246
20611
|
import { appendFileSync as appendFileSync3, readFileSync as readFileSync14, existsSync as existsSync13 } from "fs";
|
|
20247
20612
|
import { join as join16 } from "path";
|
|
20248
20613
|
import { homedir as homedir8 } from "os";
|
|
@@ -20261,7 +20626,7 @@ function appendPattern(pattern) {
|
|
|
20261
20626
|
const content = existsSync13(p) ? readFileSync14(p, "utf-8") : "";
|
|
20262
20627
|
const lines = content.split("\n").filter((l) => l.length > 0);
|
|
20263
20628
|
if (lines.includes(pattern)) {
|
|
20264
|
-
console.log(
|
|
20629
|
+
console.log(chalk13.yellow(`
|
|
20265
20630
|
Pattern already exists: ${pattern}
|
|
20266
20631
|
`));
|
|
20267
20632
|
return;
|
|
@@ -20269,26 +20634,26 @@ function appendPattern(pattern) {
|
|
|
20269
20634
|
const prefix = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
|
|
20270
20635
|
appendFileSync3(p, `${prefix}${pattern}
|
|
20271
20636
|
`);
|
|
20272
|
-
console.log(
|
|
20637
|
+
console.log(chalk13.green(`
|
|
20273
20638
|
\u2713 Added to ${IGNORE_FILE}: ${pattern}
|
|
20274
20639
|
`));
|
|
20275
20640
|
}
|
|
20276
20641
|
function ignoreCommand() {
|
|
20277
|
-
return new
|
|
20642
|
+
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
20643
|
if (opts.list) {
|
|
20279
20644
|
const patterns = readIgnoreFile();
|
|
20280
20645
|
if (patterns.length === 0) {
|
|
20281
|
-
console.log(
|
|
20646
|
+
console.log(chalk13.dim(`
|
|
20282
20647
|
No ${IGNORE_FILE} found or file is empty.
|
|
20283
20648
|
`));
|
|
20284
20649
|
return;
|
|
20285
20650
|
}
|
|
20286
|
-
console.log(
|
|
20651
|
+
console.log(chalk13.cyan(`
|
|
20287
20652
|
${IGNORE_FILE}:
|
|
20288
20653
|
`));
|
|
20289
20654
|
for (const line of patterns) {
|
|
20290
20655
|
if (line.startsWith("#")) {
|
|
20291
|
-
console.log(
|
|
20656
|
+
console.log(chalk13.dim(` ${line}`));
|
|
20292
20657
|
} else {
|
|
20293
20658
|
console.log(` ${line}`);
|
|
20294
20659
|
}
|
|
@@ -20298,10 +20663,10 @@ function ignoreCommand() {
|
|
|
20298
20663
|
}
|
|
20299
20664
|
if (opts.lastFound) {
|
|
20300
20665
|
if (!existsSync13(LAST_SCAN_PATH)) {
|
|
20301
|
-
console.error(
|
|
20666
|
+
console.error(chalk13.red(`
|
|
20302
20667
|
\u2717 No last scan results found at ${LAST_SCAN_PATH}
|
|
20303
20668
|
`));
|
|
20304
|
-
console.error(
|
|
20669
|
+
console.error(chalk13.dim(" Run 'pretense scan <file>' first.\n"));
|
|
20305
20670
|
process.exit(1);
|
|
20306
20671
|
}
|
|
20307
20672
|
try {
|
|
@@ -20309,7 +20674,7 @@ function ignoreCommand() {
|
|
|
20309
20674
|
const data = JSON.parse(raw2);
|
|
20310
20675
|
const matches = data.matches ?? [];
|
|
20311
20676
|
if (matches.length === 0) {
|
|
20312
|
-
console.log(
|
|
20677
|
+
console.log(chalk13.green("\n \u2713 Last scan was clean - nothing to ignore.\n"));
|
|
20313
20678
|
return;
|
|
20314
20679
|
}
|
|
20315
20680
|
let added = 0;
|
|
@@ -20320,11 +20685,11 @@ function ignoreCommand() {
|
|
|
20320
20685
|
added++;
|
|
20321
20686
|
}
|
|
20322
20687
|
}
|
|
20323
|
-
console.log(
|
|
20688
|
+
console.log(chalk13.green(`
|
|
20324
20689
|
\u2713 Added ${added} pattern(s) from last scan.
|
|
20325
20690
|
`));
|
|
20326
20691
|
} catch {
|
|
20327
|
-
console.error(
|
|
20692
|
+
console.error(chalk13.red(`
|
|
20328
20693
|
\u2717 Failed to parse ${LAST_SCAN_PATH}
|
|
20329
20694
|
`));
|
|
20330
20695
|
process.exit(1);
|
|
@@ -20332,7 +20697,7 @@ function ignoreCommand() {
|
|
|
20332
20697
|
return;
|
|
20333
20698
|
}
|
|
20334
20699
|
if (!pattern) {
|
|
20335
|
-
console.error(
|
|
20700
|
+
console.error(chalk13.red("\n \u2717 Please provide a pattern or use --last-found\n"));
|
|
20336
20701
|
process.exit(1);
|
|
20337
20702
|
}
|
|
20338
20703
|
appendPattern(pattern);
|
|
@@ -20341,7 +20706,7 @@ function ignoreCommand() {
|
|
|
20341
20706
|
|
|
20342
20707
|
// src/commands/completion.ts
|
|
20343
20708
|
init_esm_shims();
|
|
20344
|
-
import { Command as
|
|
20709
|
+
import { Command as Command14 } from "commander";
|
|
20345
20710
|
var ZSH_COMPLETION = `
|
|
20346
20711
|
# Pretense zsh completion
|
|
20347
20712
|
_pretense() {
|
|
@@ -20533,7 +20898,7 @@ complete -c pretense -f -n "__fish_seen_subcommand_from completion" -a bash -d "
|
|
|
20533
20898
|
complete -c pretense -f -n "__fish_seen_subcommand_from completion" -a fish -d "fish completion script"
|
|
20534
20899
|
`.trim();
|
|
20535
20900
|
function completionCommand() {
|
|
20536
|
-
return new
|
|
20901
|
+
return new Command14("completion").description("Print shell tab-completion script").argument("<shell>", "Shell to generate completion for: zsh, bash, fish").action((shell) => {
|
|
20537
20902
|
switch (shell) {
|
|
20538
20903
|
case "zsh":
|
|
20539
20904
|
console.log(ZSH_COMPLETION);
|
|
@@ -20553,10 +20918,10 @@ function completionCommand() {
|
|
|
20553
20918
|
|
|
20554
20919
|
// src/commands/quickstart.ts
|
|
20555
20920
|
init_esm_shims();
|
|
20556
|
-
import { Command as
|
|
20557
|
-
import
|
|
20921
|
+
import { Command as Command15 } from "commander";
|
|
20922
|
+
import chalk14 from "chalk";
|
|
20558
20923
|
import { existsSync as existsSync14, writeFileSync as writeFileSync6 } from "fs";
|
|
20559
|
-
import { join as join17, resolve as
|
|
20924
|
+
import { join as join17, resolve as resolve5 } from "path";
|
|
20560
20925
|
var DEFAULT_CONFIG_YAML = `scan:
|
|
20561
20926
|
secrets: block
|
|
20562
20927
|
pii: redact
|
|
@@ -20582,63 +20947,65 @@ customPatterns: []
|
|
|
20582
20947
|
verbose: false
|
|
20583
20948
|
`;
|
|
20584
20949
|
function quickstartCommand() {
|
|
20585
|
-
return new
|
|
20586
|
-
const dir =
|
|
20587
|
-
console.log(
|
|
20588
|
-
console.log(
|
|
20589
|
-
console.log(
|
|
20950
|
+
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) => {
|
|
20951
|
+
const dir = resolve5(opts.dir);
|
|
20952
|
+
console.log(chalk14.bold.cyan("\n Pretense Quickstart\n"));
|
|
20953
|
+
console.log(chalk14.dim(" Setting up AI firewall for this project...\n"));
|
|
20954
|
+
console.log(chalk14.dim(" [1/4] Scanning codebase..."));
|
|
20590
20955
|
let fp;
|
|
20591
20956
|
try {
|
|
20592
20957
|
fp = await learn({ dir, minOccurrences: 1 });
|
|
20593
|
-
console.log(
|
|
20958
|
+
console.log(chalk14.green(` ${fp.filesScanned} files | ${fp.functions.length} functions | ${fp.classes.length} classes`));
|
|
20594
20959
|
} catch (err) {
|
|
20595
|
-
console.error(
|
|
20960
|
+
console.error(chalk14.red(` \u2717 Scan failed: ${err.message}`));
|
|
20596
20961
|
process.exit(1);
|
|
20597
20962
|
}
|
|
20598
|
-
console.log(
|
|
20963
|
+
console.log(chalk14.dim(" [2/4] Writing config..."));
|
|
20599
20964
|
const configPath = join17(dir, "pretense.yaml");
|
|
20600
20965
|
if (!opts.dryRun) {
|
|
20601
20966
|
if (!existsSync14(configPath)) {
|
|
20602
20967
|
writeFileSync6(configPath, DEFAULT_CONFIG_YAML);
|
|
20603
|
-
console.log(
|
|
20968
|
+
console.log(chalk14.green(" Created pretense.yaml"));
|
|
20604
20969
|
} else {
|
|
20605
|
-
console.log(
|
|
20970
|
+
console.log(chalk14.dim(" pretense.yaml already exists, skipping"));
|
|
20606
20971
|
}
|
|
20607
20972
|
}
|
|
20608
|
-
console.log(
|
|
20973
|
+
console.log(chalk14.dim(" [3/4] Updating .gitignore..."));
|
|
20609
20974
|
if (!opts.dryRun) {
|
|
20610
20975
|
const gitignorePath = join17(dir, ".gitignore");
|
|
20611
20976
|
let content = existsSync14(gitignorePath) ? (await import("fs")).default.readFileSync(gitignorePath, "utf-8") : "";
|
|
20612
20977
|
if (!content.includes(".pretense/")) {
|
|
20613
20978
|
content = content + (content.endsWith("\n") ? "" : "\n") + ".pretense/\n";
|
|
20614
20979
|
writeFileSync6(gitignorePath, content);
|
|
20615
|
-
console.log(
|
|
20980
|
+
console.log(chalk14.green(" Added .pretense/ to .gitignore"));
|
|
20616
20981
|
} 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(` ${
|
|
20982
|
+
console.log(chalk14.dim(" .gitignore already includes .pretense/"));
|
|
20983
|
+
}
|
|
20984
|
+
}
|
|
20985
|
+
console.log(chalk14.dim(" [4/4] Done.\n"));
|
|
20986
|
+
console.log(chalk14.bold(" Next steps:\n"));
|
|
20987
|
+
console.log(` ${chalk14.green("1.")} ${chalk14.cyan('pretense run claude "build me X"')} Run any AI tool through the proxy \u2014 zero setup`);
|
|
20988
|
+
console.log(` ${chalk14.dim("(auto-starts the proxy on a free port and sets the base-URL env var for you)")}`);
|
|
20989
|
+
console.log(` ${chalk14.dim("Works the same for any tool: ")}${chalk14.cyan("pretense run cursor")}${chalk14.dim(", ")}${chalk14.cyan("pretense run gemini")}`);
|
|
20990
|
+
console.log(` ${chalk14.green("2.")} ${chalk14.cyan("pretense install")} Install pre-commit + pre-push git hooks`);
|
|
20991
|
+
console.log(` ${chalk14.green("3.")} ${chalk14.dim("Prefer to wire it by hand? Set the base URL in the SAME command as the tool:")}`);
|
|
20992
|
+
console.log(` ${chalk14.dim("ANTHROPIC_BASE_URL=http://localhost:9339 claude")}`);
|
|
20993
|
+
console.log(` ${chalk14.dim("OPENAI_BASE_URL=http://localhost:9339/v1 cursor ...")}`);
|
|
20994
|
+
console.log(` ${chalk14.dim("GOOGLE_GEMINI_BASE_URL=http://localhost:9339 gemini ...")}`);
|
|
20995
|
+
console.log(` ${chalk14.bold.yellow("\u26A0 Set this in the SAME terminal as your AI tool \u2014 a new terminal is NOT protected.")}`);
|
|
20996
|
+
console.log(` ${chalk14.bold.yellow(" Simplest: use ")}${chalk14.cyan("pretense run")}${chalk14.bold.yellow(" and skip the export entirely.")}
|
|
20630
20997
|
`);
|
|
20631
|
-
console.log(
|
|
20998
|
+
console.log(chalk14.bold.green(" Protected. Happy coding.\n"));
|
|
20632
20999
|
});
|
|
20633
21000
|
}
|
|
20634
21001
|
|
|
20635
21002
|
// src/commands/mutate.ts
|
|
20636
21003
|
init_esm_shims();
|
|
20637
|
-
import { Command as
|
|
20638
|
-
import
|
|
21004
|
+
import { Command as Command16, Option as Option2 } from "commander";
|
|
21005
|
+
import chalk16 from "chalk";
|
|
20639
21006
|
import { readFileSync as readFileSync15, writeFileSync as writeFileSync7, chmodSync as chmodSync4, existsSync as existsSync15 } from "fs";
|
|
20640
21007
|
import { createHash as createHash3 } from "crypto";
|
|
20641
|
-
import { resolve as
|
|
21008
|
+
import { resolve as resolve6, extname as extname4 } from "path";
|
|
20642
21009
|
|
|
20643
21010
|
// src/secret-crypto.ts
|
|
20644
21011
|
init_esm_shims();
|
|
@@ -20761,6 +21128,48 @@ function ensureDefaultMapPath(absFilePath, override2) {
|
|
|
20761
21128
|
return defaultMapPath(absFilePath, override2);
|
|
20762
21129
|
}
|
|
20763
21130
|
|
|
21131
|
+
// src/local-audit.ts
|
|
21132
|
+
init_esm_shims();
|
|
21133
|
+
import { homedir as homedir10 } from "os";
|
|
21134
|
+
import { join as join21 } from "path";
|
|
21135
|
+
import { randomUUID } from "crypto";
|
|
21136
|
+
import chalk15 from "chalk";
|
|
21137
|
+
function localAuditDbPath() {
|
|
21138
|
+
return join21(homedir10(), ".pretense", "audit.db");
|
|
21139
|
+
}
|
|
21140
|
+
function recordLocalAudit(rec, dbPath = localAuditDbPath()) {
|
|
21141
|
+
let store = null;
|
|
21142
|
+
try {
|
|
21143
|
+
store = new AuditStore(dbPath);
|
|
21144
|
+
store.logEntry({
|
|
21145
|
+
timestamp: Date.now(),
|
|
21146
|
+
requestId: randomUUID(),
|
|
21147
|
+
provider: "cli",
|
|
21148
|
+
model: "-",
|
|
21149
|
+
direction: "outbound",
|
|
21150
|
+
originalSize: rec.originalSize,
|
|
21151
|
+
processedSize: rec.processedSize,
|
|
21152
|
+
secretsBlocked: rec.secretsBlocked,
|
|
21153
|
+
piiRedacted: rec.piiRedacted,
|
|
21154
|
+
mutationsApplied: rec.mutationsApplied,
|
|
21155
|
+
scanDurationMs: 0,
|
|
21156
|
+
mutationDurationMs: rec.durationMs,
|
|
21157
|
+
action: rec.action,
|
|
21158
|
+
details: { source: rec.action === "reversed" ? "cli-reverse" : "cli-mutate", file: rec.file },
|
|
21159
|
+
...rec.detectorKinds && rec.detectorKinds.length > 0 ? { detectorKinds: rec.detectorKinds } : {}
|
|
21160
|
+
});
|
|
21161
|
+
return true;
|
|
21162
|
+
} catch (err) {
|
|
21163
|
+
console.error(
|
|
21164
|
+
chalk15.yellow(` ! Could not record this ${rec.action === "reversed" ? "reversal" : "mutation"} in the audit log`) + chalk15.dim(` (${err.message}).`) + chalk15.dim(`
|
|
21165
|
+
The file was still ${rec.action === "reversed" ? "restored" : "mutated"}; only the audit entry is missing.`)
|
|
21166
|
+
);
|
|
21167
|
+
return false;
|
|
21168
|
+
} finally {
|
|
21169
|
+
store?.close();
|
|
21170
|
+
}
|
|
21171
|
+
}
|
|
21172
|
+
|
|
20764
21173
|
// src/commands/mutate.ts
|
|
20765
21174
|
var REVERSAL_MAP_VERSION = 3;
|
|
20766
21175
|
function preflightReversalMap(mapPath, absFile, content, contentSha, force) {
|
|
@@ -20781,7 +21190,7 @@ function preflightReversalMap(mapPath, absFile, content, contentSha, force) {
|
|
|
20781
21190
|
to overwrite it deliberately.`
|
|
20782
21191
|
};
|
|
20783
21192
|
}
|
|
20784
|
-
const existingFile = existing.file ?
|
|
21193
|
+
const existingFile = existing.file ? resolve6(existing.file) : void 0;
|
|
20785
21194
|
if (existingFile && existingFile !== absFile) {
|
|
20786
21195
|
if (force) {
|
|
20787
21196
|
return { action: "write", generation: 1, supersededSha: existing.originalSha };
|
|
@@ -20857,8 +21266,10 @@ async function collectMutationFindings(content, file, level, presetName) {
|
|
|
20857
21266
|
const cannotMutate = (type) => {
|
|
20858
21267
|
unmutatable.set(type, (unmutatable.get(type) ?? 0) + 1);
|
|
20859
21268
|
};
|
|
21269
|
+
const categoryByType = /* @__PURE__ */ new Map();
|
|
20860
21270
|
for (const m of result.matches) {
|
|
20861
21271
|
if (level === 2 && m.level !== 2) continue;
|
|
21272
|
+
if (!categoryByType.has(m.type)) categoryByType.set(m.type, m.category);
|
|
20862
21273
|
const offset = m.start;
|
|
20863
21274
|
const length = m.end - m.start;
|
|
20864
21275
|
if (length <= 0) {
|
|
@@ -20871,18 +21282,18 @@ async function collectMutationFindings(content, file, level, presetName) {
|
|
|
20871
21282
|
}
|
|
20872
21283
|
findings.push({ value: m.value, offset, length, type: m.type });
|
|
20873
21284
|
}
|
|
20874
|
-
return { findings, unmutatable };
|
|
21285
|
+
return { findings, unmutatable, categoryByType };
|
|
20875
21286
|
}
|
|
20876
21287
|
function reportUnmutatable(unmutatable) {
|
|
20877
21288
|
if (unmutatable.size === 0) return;
|
|
20878
21289
|
const total = [...unmutatable.values()].reduce((a, b) => a + b, 0);
|
|
20879
21290
|
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
21291
|
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`) +
|
|
21292
|
+
chalk16.yellow(`
|
|
21293
|
+
! ${total} detected value${total === 1 ? "" : "s"} could NOT be mutated: ${kinds}`) + chalk16.dim(`
|
|
21294
|
+
These were found only inside an encoded form (base64/hex) or at a span`) + chalk16.dim(`
|
|
21295
|
+
that does not map back to the file's bytes, so there is no range to`) + chalk16.dim(`
|
|
21296
|
+
rewrite. They are STILL PRESENT in the output. Run \`pretense scan\` on`) + chalk16.dim(`
|
|
20886
21297
|
this file to see them, and remove or re-encode them by hand.
|
|
20887
21298
|
`)
|
|
20888
21299
|
);
|
|
@@ -20892,9 +21303,9 @@ function writeReversalMap(mapPath, mapFile) {
|
|
|
20892
21303
|
writeFileSync7(mapPath, JSON.stringify(mapFile, null, 2), { mode: 384 });
|
|
20893
21304
|
} catch (e) {
|
|
20894
21305
|
console.error(
|
|
20895
|
-
|
|
20896
|
-
x Cannot write reversal map: ${mapPath}`) +
|
|
20897
|
-
${e.message}`) +
|
|
21306
|
+
chalk16.red(`
|
|
21307
|
+
x Cannot write reversal map: ${mapPath}`) + chalk16.dim(`
|
|
21308
|
+
${e.message}`) + chalk16.dim(`
|
|
20898
21309
|
Nothing was mutated \u2014 without a map the original could not be restored.
|
|
20899
21310
|
`)
|
|
20900
21311
|
);
|
|
@@ -20905,7 +21316,7 @@ function writeReversalMap(mapPath, mapFile) {
|
|
|
20905
21316
|
} catch {
|
|
20906
21317
|
}
|
|
20907
21318
|
console.error(
|
|
20908
|
-
|
|
21319
|
+
chalk16.green(` \u2713 Wrote encrypted reversal map \u2192 ${mapPath}`) + chalk16.dim(" (AES-256-GCM, mode 0600)")
|
|
20909
21320
|
);
|
|
20910
21321
|
return 0;
|
|
20911
21322
|
}
|
|
@@ -20914,7 +21325,7 @@ async function runMutate(file, opts = {}) {
|
|
|
20914
21325
|
try {
|
|
20915
21326
|
raw2 = readFileSync15(file);
|
|
20916
21327
|
} catch {
|
|
20917
|
-
console.error(
|
|
21328
|
+
console.error(chalk16.red(`
|
|
20918
21329
|
x Cannot read file: ${file}
|
|
20919
21330
|
`));
|
|
20920
21331
|
return 1;
|
|
@@ -20922,9 +21333,9 @@ async function runMutate(file, opts = {}) {
|
|
|
20922
21333
|
const content = raw2.toString("utf-8");
|
|
20923
21334
|
if (!Buffer.from(content, "utf-8").equals(raw2)) {
|
|
20924
21335
|
console.error(
|
|
20925
|
-
|
|
20926
|
-
x Not valid UTF-8: ${file}`) +
|
|
20927
|
-
Mutating would replace the undecodable bytes with U+FFFD and the`) +
|
|
21336
|
+
chalk16.red(`
|
|
21337
|
+
x Not valid UTF-8: ${file}`) + chalk16.dim(`
|
|
21338
|
+
Mutating would replace the undecodable bytes with U+FFFD and the`) + chalk16.dim(`
|
|
20928
21339
|
original content could not be restored. File left untouched.
|
|
20929
21340
|
`)
|
|
20930
21341
|
);
|
|
@@ -20932,7 +21343,8 @@ async function runMutate(file, opts = {}) {
|
|
|
20932
21343
|
}
|
|
20933
21344
|
const preview = opts.stdout === true;
|
|
20934
21345
|
const inPlace = !preview && !opts.output;
|
|
20935
|
-
const
|
|
21346
|
+
const mutateStartMs = Date.now();
|
|
21347
|
+
const { findings, unmutatable, categoryByType } = await collectMutationFindings(
|
|
20936
21348
|
content,
|
|
20937
21349
|
file,
|
|
20938
21350
|
opts.level,
|
|
@@ -20940,7 +21352,7 @@ async function runMutate(file, opts = {}) {
|
|
|
20940
21352
|
);
|
|
20941
21353
|
if (findings.length === 0) {
|
|
20942
21354
|
if (unmutatable.size === 0) {
|
|
20943
|
-
console.error(
|
|
21355
|
+
console.error(chalk16.dim(` No secrets found in ${file} \u2014 nothing to mutate.`));
|
|
20944
21356
|
} else {
|
|
20945
21357
|
reportUnmutatable(unmutatable);
|
|
20946
21358
|
}
|
|
@@ -20948,8 +21360,8 @@ async function runMutate(file, opts = {}) {
|
|
|
20948
21360
|
return 0;
|
|
20949
21361
|
}
|
|
20950
21362
|
reportUnmutatable(unmutatable);
|
|
20951
|
-
const absFile =
|
|
20952
|
-
const projectRoot =
|
|
21363
|
+
const absFile = resolve6(file);
|
|
21364
|
+
const projectRoot = resolveProjectRoot(absFile);
|
|
20953
21365
|
const result = mutateSecrets(content, findings, { projectRoot, keysDir: opts.keysDir });
|
|
20954
21366
|
let effectiveMap;
|
|
20955
21367
|
if (preview) {
|
|
@@ -20964,9 +21376,9 @@ async function runMutate(file, opts = {}) {
|
|
|
20964
21376
|
const pre = preflightReversalMap(effectiveMap, absFile, content, contentSha, opts.forceMap === true);
|
|
20965
21377
|
if (pre.action === "refuse") {
|
|
20966
21378
|
console.error(
|
|
20967
|
-
|
|
20968
|
-
x ${pre.code}: refusing to overwrite an existing reversal map.`) +
|
|
20969
|
-
${pre.message}`) +
|
|
21379
|
+
chalk16.red(`
|
|
21380
|
+
x ${pre.code}: refusing to overwrite an existing reversal map.`) + chalk16.dim(`
|
|
21381
|
+
${pre.message}`) + chalk16.dim(`
|
|
20970
21382
|
Nothing was mutated. Both the file and the map are untouched.
|
|
20971
21383
|
`)
|
|
20972
21384
|
);
|
|
@@ -20976,8 +21388,8 @@ async function runMutate(file, opts = {}) {
|
|
|
20976
21388
|
const isNoop = pre.action === "keep" && pre.expectedMutatedSha === mutatedSha;
|
|
20977
21389
|
if (isNoop) {
|
|
20978
21390
|
console.error(
|
|
20979
|
-
|
|
20980
|
-
hash \u2014 left unchanged (mutation is deterministic, so a rewrite would be`) +
|
|
21391
|
+
chalk16.dim(` = Reversal map ${effectiveMap} already describes ${file} at this exact content`) + chalk16.dim(`
|
|
21392
|
+
hash \u2014 left unchanged (mutation is deterministic, so a rewrite would be`) + chalk16.dim(`
|
|
20981
21393
|
byte-identical).`)
|
|
20982
21394
|
);
|
|
20983
21395
|
} else {
|
|
@@ -21003,10 +21415,10 @@ async function runMutate(file, opts = {}) {
|
|
|
21003
21415
|
if (written !== 0) return written;
|
|
21004
21416
|
if (pre.action === "write" && pre.generation > 1) {
|
|
21005
21417
|
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`) +
|
|
21418
|
+
chalk16.yellow(`
|
|
21419
|
+
! --force-map: this map is mutation GENERATION ${pre.generation}.`) + chalk16.dim(`
|
|
21420
|
+
Its "original" values are synthetics from the previous round, so`) + chalk16.dim(`
|
|
21421
|
+
reversing with it returns the EARLIER MUTATED file, not your real`) + chalk16.dim(`
|
|
21010
21422
|
secrets. \`pretense reverse\` will refuse it unless --allow-partial.
|
|
21011
21423
|
`)
|
|
21012
21424
|
);
|
|
@@ -21015,8 +21427,8 @@ async function runMutate(file, opts = {}) {
|
|
|
21015
21427
|
}
|
|
21016
21428
|
if (inPlace && !effectiveMap) {
|
|
21017
21429
|
console.error(
|
|
21018
|
-
|
|
21019
|
-
x Refusing to mutate ${file} in place with no reversal map \u2014 this would be irreversible.`) +
|
|
21430
|
+
chalk16.red(`
|
|
21431
|
+
x Refusing to mutate ${file} in place with no reversal map \u2014 this would be irreversible.`) + chalk16.dim(`
|
|
21020
21432
|
Pass --map <path>, or report this as a bug.
|
|
21021
21433
|
`)
|
|
21022
21434
|
);
|
|
@@ -21025,29 +21437,48 @@ async function runMutate(file, opts = {}) {
|
|
|
21025
21437
|
try {
|
|
21026
21438
|
if (inPlace) {
|
|
21027
21439
|
writeFileAtomicSync(file, result.content);
|
|
21028
|
-
console.error(
|
|
21440
|
+
console.error(chalk16.green(` \u2713 Mutated ${result.mutations.length} secrets in ${file} (in-place)`));
|
|
21029
21441
|
} else if (opts.output) {
|
|
21030
21442
|
writeFileAtomicSync(opts.output, result.content);
|
|
21031
|
-
console.error(
|
|
21443
|
+
console.error(chalk16.green(` \u2713 Mutated ${result.mutations.length} secrets \u2192 ${opts.output}`));
|
|
21032
21444
|
} else {
|
|
21033
21445
|
process.stdout.write(result.content);
|
|
21034
21446
|
}
|
|
21035
21447
|
} catch (e) {
|
|
21036
21448
|
console.error(
|
|
21037
|
-
|
|
21038
|
-
x Cannot write mutated content: ${opts.output ?? file}`) +
|
|
21449
|
+
chalk16.red(`
|
|
21450
|
+
x Cannot write mutated content: ${opts.output ?? file}`) + chalk16.dim(`
|
|
21039
21451
|
${e.message}
|
|
21040
21452
|
`)
|
|
21041
21453
|
);
|
|
21042
21454
|
return 1;
|
|
21043
21455
|
}
|
|
21456
|
+
if (!preview && result.mutations.length > 0) {
|
|
21457
|
+
let secretsBlocked = 0;
|
|
21458
|
+
let piiRedacted = 0;
|
|
21459
|
+
for (const m of result.mutations) {
|
|
21460
|
+
if (categoryByType.get(m.type) === "pii") piiRedacted += 1;
|
|
21461
|
+
else secretsBlocked += 1;
|
|
21462
|
+
}
|
|
21463
|
+
recordLocalAudit({
|
|
21464
|
+
action: "mutated",
|
|
21465
|
+
mutationsApplied: result.mutations.length,
|
|
21466
|
+
secretsBlocked,
|
|
21467
|
+
piiRedacted,
|
|
21468
|
+
originalSize: Buffer.byteLength(content, "utf-8"),
|
|
21469
|
+
processedSize: Buffer.byteLength(result.content, "utf-8"),
|
|
21470
|
+
durationMs: Date.now() - mutateStartMs,
|
|
21471
|
+
detectorKinds: [...new Set(result.mutations.map((m) => m.type))],
|
|
21472
|
+
file: absFile
|
|
21473
|
+
});
|
|
21474
|
+
}
|
|
21044
21475
|
if (opts.showMap) {
|
|
21045
21476
|
console.error(JSON.stringify(result.mutations, null, 2));
|
|
21046
21477
|
}
|
|
21047
21478
|
return 0;
|
|
21048
21479
|
}
|
|
21049
21480
|
function mutateCommand() {
|
|
21050
|
-
return new
|
|
21481
|
+
return new Command16("mutate").description(
|
|
21051
21482
|
"Mutate secrets IN PLACE and auto-write an encrypted reversal map (reverse with `pretense reverse <file>`; --stdout to preview instead)"
|
|
21052
21483
|
).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
21484
|
"--map <path>",
|
|
@@ -21068,7 +21499,7 @@ function mutateCommand() {
|
|
|
21068
21499
|
const resolved = resolvePolicy(opts.policy);
|
|
21069
21500
|
if (resolved?.scannerPreset) opts.presetName = resolved.scannerPreset;
|
|
21070
21501
|
} catch (err) {
|
|
21071
|
-
console.error(
|
|
21502
|
+
console.error(chalk16.red(`
|
|
21072
21503
|
x ${err.message}
|
|
21073
21504
|
`));
|
|
21074
21505
|
process.exit(1);
|
|
@@ -21080,10 +21511,10 @@ function mutateCommand() {
|
|
|
21080
21511
|
|
|
21081
21512
|
// src/commands/reverse.ts
|
|
21082
21513
|
init_esm_shims();
|
|
21083
|
-
import { Command as
|
|
21084
|
-
import
|
|
21514
|
+
import { Command as Command17 } from "commander";
|
|
21515
|
+
import chalk17 from "chalk";
|
|
21085
21516
|
import { readFileSync as readFileSync16, existsSync as existsSync16, statSync as statSync6 } from "fs";
|
|
21086
|
-
import { resolve as
|
|
21517
|
+
import { resolve as resolve7 } from "path";
|
|
21087
21518
|
import { createHash as createHash4 } from "crypto";
|
|
21088
21519
|
function sha256Hex3(s) {
|
|
21089
21520
|
return createHash4("sha256").update(s, "utf8").digest("hex");
|
|
@@ -21115,14 +21546,14 @@ function loadReversalMap(mapPath) {
|
|
|
21115
21546
|
try {
|
|
21116
21547
|
parsed = JSON.parse(readFileSync16(mapPath, "utf-8"));
|
|
21117
21548
|
} catch {
|
|
21118
|
-
console.error(
|
|
21549
|
+
console.error(chalk17.red(`
|
|
21119
21550
|
x Cannot read reversal map: ${mapPath}
|
|
21120
21551
|
`));
|
|
21121
21552
|
process.exit(1);
|
|
21122
21553
|
}
|
|
21123
21554
|
const map = parsed;
|
|
21124
21555
|
if (!map || !Array.isArray(map.mutations)) {
|
|
21125
|
-
console.error(
|
|
21556
|
+
console.error(chalk17.red(`
|
|
21126
21557
|
x Malformed reversal map (missing "mutations"): ${mapPath}
|
|
21127
21558
|
`));
|
|
21128
21559
|
process.exit(1);
|
|
@@ -21130,9 +21561,9 @@ function loadReversalMap(mapPath) {
|
|
|
21130
21561
|
const version = typeof map.version === "number" ? map.version : 1;
|
|
21131
21562
|
if (version > REVERSAL_MAP_VERSION) {
|
|
21132
21563
|
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.`) +
|
|
21564
|
+
chalk17.red(`
|
|
21565
|
+
x Reversal map is version ${version}; this build understands up to ${REVERSAL_MAP_VERSION}.`) + chalk17.dim(`
|
|
21566
|
+
It was written by a newer Pretense. Upgrade the CLI to reverse it.`) + chalk17.dim(`
|
|
21136
21567
|
File left untouched.
|
|
21137
21568
|
`)
|
|
21138
21569
|
);
|
|
@@ -21141,9 +21572,10 @@ function loadReversalMap(mapPath) {
|
|
|
21141
21572
|
return map;
|
|
21142
21573
|
}
|
|
21143
21574
|
function runReverse(file, opts = {}) {
|
|
21144
|
-
const
|
|
21575
|
+
const reverseStartMs = Date.now();
|
|
21576
|
+
const absPath = resolve7(file);
|
|
21145
21577
|
if (!existsSync16(absPath) || !statSync6(absPath).isFile()) {
|
|
21146
|
-
console.error(
|
|
21578
|
+
console.error(chalk17.red(`
|
|
21147
21579
|
x File not found: ${file}
|
|
21148
21580
|
`));
|
|
21149
21581
|
return 1;
|
|
@@ -21151,11 +21583,11 @@ function runReverse(file, opts = {}) {
|
|
|
21151
21583
|
const mapPath = opts.map ?? defaultMapPath(absPath, opts.mapsDir);
|
|
21152
21584
|
if (!opts.map && !existsSync16(mapPath)) {
|
|
21153
21585
|
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 ${
|
|
21586
|
+
chalk17.red(`
|
|
21587
|
+
x No reversal map found for ${absPath}`) + chalk17.dim(`
|
|
21588
|
+
Looked in the default location: ${mapPath}`) + chalk17.dim(`
|
|
21589
|
+
Mutate the file first with ${chalk17.cyan(`pretense mutate ${file}`)}, or pass`) + chalk17.dim(`
|
|
21590
|
+
an explicit ${chalk17.cyan("--map <path>")} if you wrote the map somewhere else.
|
|
21159
21591
|
`)
|
|
21160
21592
|
);
|
|
21161
21593
|
return 1;
|
|
@@ -21163,30 +21595,41 @@ function runReverse(file, opts = {}) {
|
|
|
21163
21595
|
const mapFile = loadReversalMap(mapPath);
|
|
21164
21596
|
let plainMutations;
|
|
21165
21597
|
if (mapFile.encrypted) {
|
|
21166
|
-
const
|
|
21167
|
-
const
|
|
21168
|
-
|
|
21169
|
-
|
|
21170
|
-
|
|
21171
|
-
|
|
21172
|
-
|
|
21173
|
-
|
|
21174
|
-
|
|
21175
|
-
|
|
21598
|
+
const keySource = mapFile.file ?? absPath;
|
|
21599
|
+
const roots = candidateProjectRoots(keySource);
|
|
21600
|
+
let decrypted;
|
|
21601
|
+
let anyKeyPresent = false;
|
|
21602
|
+
for (const root of roots) {
|
|
21603
|
+
const key = readProjectKey(root, opts.keysDir);
|
|
21604
|
+
if (!key) continue;
|
|
21605
|
+
anyKeyPresent = true;
|
|
21606
|
+
try {
|
|
21607
|
+
decrypted = mapFile.mutations.map((m) => ({
|
|
21608
|
+
original: decryptSecret(m.original, key),
|
|
21609
|
+
replacement: m.replacement
|
|
21610
|
+
}));
|
|
21611
|
+
break;
|
|
21612
|
+
} catch {
|
|
21613
|
+
decrypted = void 0;
|
|
21614
|
+
}
|
|
21176
21615
|
}
|
|
21177
|
-
|
|
21178
|
-
|
|
21179
|
-
original: decryptSecret(m.original, key),
|
|
21180
|
-
replacement: m.replacement
|
|
21181
|
-
}));
|
|
21182
|
-
} catch {
|
|
21616
|
+
if (!decrypted) {
|
|
21617
|
+
const looked = roots.join("\n ");
|
|
21183
21618
|
console.error(
|
|
21184
|
-
|
|
21185
|
-
x
|
|
21619
|
+
chalk17.red(`
|
|
21620
|
+
x Encryption key not found for this file \u2014 cannot decrypt the reversal map.`) + (anyKeyPresent ? chalk17.dim(`
|
|
21621
|
+
A key was present but did not decrypt this map (wrong key or corrupt data).`) : chalk17.dim(`
|
|
21622
|
+
No key was found under any known project location for this file.`)) + chalk17.dim(`
|
|
21623
|
+
Looked under these project roots (keys dir ${opts.keysDir ?? "~/.pretense/keys"}):`) + chalk17.dim(`
|
|
21624
|
+
${looked}`) + chalk17.dim(`
|
|
21625
|
+
Run \`reverse\` on the SAME machine and HOME where you mutated the file,`) + chalk17.dim(`
|
|
21626
|
+
or set ${chalk17.cyan("PRETENSE_PROJECT_ROOT")} to the project root used at mutate time.`) + chalk17.dim(`
|
|
21627
|
+
File left untouched.
|
|
21186
21628
|
`)
|
|
21187
21629
|
);
|
|
21188
21630
|
return 1;
|
|
21189
21631
|
}
|
|
21632
|
+
plainMutations = decrypted;
|
|
21190
21633
|
} else {
|
|
21191
21634
|
plainMutations = mapFile.mutations.map((m) => ({ original: m.original, replacement: m.replacement }));
|
|
21192
21635
|
}
|
|
@@ -21195,10 +21638,10 @@ function runReverse(file, opts = {}) {
|
|
|
21195
21638
|
const absentCount = plainMutations.length - appliedCount;
|
|
21196
21639
|
if (typeof mapFile.mutatedSha === "string" && mapFile.mutatedSha !== sha256Hex3(mutatedContent)) {
|
|
21197
21640
|
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)}`) +
|
|
21641
|
+
chalk17.yellow(`
|
|
21642
|
+
! This file is not the exact output this map was written for.`) + chalk17.dim(`
|
|
21643
|
+
map expects sha256 ${mapFile.mutatedSha}`) + chalk17.dim(`
|
|
21644
|
+
file is sha256 ${sha256Hex3(mutatedContent)}`) + chalk17.dim(`
|
|
21202
21645
|
${absentCount} of ${plainMutations.length} mapped tokens are not present in it.
|
|
21203
21646
|
`)
|
|
21204
21647
|
);
|
|
@@ -21208,8 +21651,8 @@ function runReverse(file, opts = {}) {
|
|
|
21208
21651
|
restored = applyReverse(mutatedContent, plainMutations);
|
|
21209
21652
|
} catch (e) {
|
|
21210
21653
|
console.error(
|
|
21211
|
-
|
|
21212
|
-
x ${e.message}`) +
|
|
21654
|
+
chalk17.red(`
|
|
21655
|
+
x ${e.message}`) + chalk17.dim(`
|
|
21213
21656
|
File left untouched.
|
|
21214
21657
|
`)
|
|
21215
21658
|
);
|
|
@@ -21217,8 +21660,8 @@ function runReverse(file, opts = {}) {
|
|
|
21217
21660
|
}
|
|
21218
21661
|
if (!mapFile.originalSha) {
|
|
21219
21662
|
console.error(
|
|
21220
|
-
|
|
21221
|
-
x Reversal map has no originalSha \u2014 cannot verify round-trip integrity.`) +
|
|
21663
|
+
chalk17.red(`
|
|
21664
|
+
x Reversal map has no originalSha \u2014 cannot verify round-trip integrity.`) + chalk17.dim(`
|
|
21222
21665
|
Refusing to write. File left untouched.
|
|
21223
21666
|
`)
|
|
21224
21667
|
);
|
|
@@ -21227,10 +21670,10 @@ function runReverse(file, opts = {}) {
|
|
|
21227
21670
|
const restoredSha = sha256Hex3(restored);
|
|
21228
21671
|
if (restoredSha !== mapFile.originalSha) {
|
|
21229
21672
|
console.error(
|
|
21230
|
-
|
|
21231
|
-
x Reversal integrity check failed for ${absPath}`) +
|
|
21232
|
-
expected sha256 ${mapFile.originalSha}`) +
|
|
21233
|
-
got sha256 ${restoredSha}`) +
|
|
21673
|
+
chalk17.red(`
|
|
21674
|
+
x Reversal integrity check failed for ${absPath}`) + chalk17.dim(`
|
|
21675
|
+
expected sha256 ${mapFile.originalSha}`) + chalk17.dim(`
|
|
21676
|
+
got sha256 ${restoredSha}`) + chalk17.dim(`
|
|
21234
21677
|
File left untouched.
|
|
21235
21678
|
`)
|
|
21236
21679
|
);
|
|
@@ -21241,30 +21684,30 @@ function runReverse(file, opts = {}) {
|
|
|
21241
21684
|
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
21685
|
if (!opts.allowPartial) {
|
|
21243
21686
|
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).`) +
|
|
21687
|
+
chalk17.red(`
|
|
21688
|
+
x NOT A RESTORE: ${what}`) + chalk17.dim(`
|
|
21689
|
+
Its originals are themselves synthetics from an earlier mutation of this`) + chalk17.dim(`
|
|
21690
|
+
file, so applying it would return the PREVIOUS MUTATED VERSION, not your`) + chalk17.dim(`
|
|
21691
|
+
real secrets \u2014 while every integrity check passed, because the map is`) + chalk17.dim(`
|
|
21692
|
+
self-consistent with the file. Reverse with the map from the FIRST`) + chalk17.dim(`
|
|
21693
|
+
mutation round instead.`) + chalk17.dim(`
|
|
21694
|
+
--allow-partial writes it anyway (useful only to walk a chain back one`) + chalk17.dim(`
|
|
21695
|
+
generation at a time).`) + chalk17.dim(`
|
|
21253
21696
|
File left untouched.
|
|
21254
21697
|
`)
|
|
21255
21698
|
);
|
|
21256
21699
|
return 1;
|
|
21257
21700
|
}
|
|
21258
21701
|
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`) +
|
|
21702
|
+
chalk17.yellow(`
|
|
21703
|
+
! PARTIAL RESTORE (--allow-partial): ${what}`) + chalk17.dim(`
|
|
21704
|
+
The output is the PREVIOUS MUTATED GENERATION of this file, NOT the`) + chalk17.dim(`
|
|
21705
|
+
original. ${lineage.syntheticCount || "Some"} restored value(s) are still synthetic. Reverse again with`) + chalk17.dim(`
|
|
21263
21706
|
the earlier round's map to reach the real secrets.
|
|
21264
21707
|
`)
|
|
21265
21708
|
);
|
|
21266
21709
|
}
|
|
21267
|
-
const mark = lineage.ok ?
|
|
21710
|
+
const mark = lineage.ok ? chalk17.green(" \u2713 Restored") : chalk17.yellow(" ! Partially restored");
|
|
21268
21711
|
const detail = `${appliedCount} of ${plainMutations.length} mapped tokens` + (lineage.ok ? "" : " \u2014 to an earlier MUTATED generation, not the original");
|
|
21269
21712
|
const preview = opts.stdout === true;
|
|
21270
21713
|
const inPlace = !preview && !opts.output;
|
|
@@ -21277,6 +21720,19 @@ function runReverse(file, opts = {}) {
|
|
|
21277
21720
|
} else {
|
|
21278
21721
|
process.stdout.write(restored);
|
|
21279
21722
|
}
|
|
21723
|
+
if (!preview && appliedCount > 0) {
|
|
21724
|
+
recordLocalAudit({
|
|
21725
|
+
action: "reversed",
|
|
21726
|
+
mutationsApplied: appliedCount,
|
|
21727
|
+
secretsBlocked: 0,
|
|
21728
|
+
piiRedacted: 0,
|
|
21729
|
+
originalSize: Buffer.byteLength(mutatedContent, "utf-8"),
|
|
21730
|
+
processedSize: Buffer.byteLength(restored, "utf-8"),
|
|
21731
|
+
durationMs: Date.now() - reverseStartMs,
|
|
21732
|
+
detectorKinds: [...new Set(mapFile.mutations.map((m) => m.type).filter((t) => typeof t === "string"))],
|
|
21733
|
+
file: absPath
|
|
21734
|
+
});
|
|
21735
|
+
}
|
|
21280
21736
|
if (opts.json) {
|
|
21281
21737
|
console.error(
|
|
21282
21738
|
JSON.stringify({
|
|
@@ -21294,7 +21750,7 @@ function runReverse(file, opts = {}) {
|
|
|
21294
21750
|
return 0;
|
|
21295
21751
|
}
|
|
21296
21752
|
function reverseCommand() {
|
|
21297
|
-
return new
|
|
21753
|
+
return new Command17("reverse").description(
|
|
21298
21754
|
"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
21755
|
).argument("<file>", "Mutated file to restore (must be byte-identical to the mutate output)").option(
|
|
21300
21756
|
"--map <path>",
|
|
@@ -21309,8 +21765,8 @@ function reverseCommand() {
|
|
|
21309
21765
|
|
|
21310
21766
|
// src/commands/run.ts
|
|
21311
21767
|
init_esm_shims();
|
|
21312
|
-
import { Command as
|
|
21313
|
-
import
|
|
21768
|
+
import { Command as Command18 } from "commander";
|
|
21769
|
+
import chalk18 from "chalk";
|
|
21314
21770
|
import { spawn } from "child_process";
|
|
21315
21771
|
import { basename as basename3 } from "path";
|
|
21316
21772
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
@@ -21384,20 +21840,23 @@ async function waitHealthy(port, deadlineMs, pollMs = 150) {
|
|
|
21384
21840
|
}
|
|
21385
21841
|
function spawnProxy(port) {
|
|
21386
21842
|
const cliEntry = process.argv[1] ?? fileURLToPath4(import.meta.url);
|
|
21387
|
-
|
|
21843
|
+
const startArgs = ["start", "--port", String(port), "--strict-port"];
|
|
21844
|
+
const isCompiledBinary = cliEntry.includes("$bunfs") || cliEntry.includes("~BUN");
|
|
21845
|
+
const spawnArgs = isCompiledBinary ? startArgs : [cliEntry, ...startArgs];
|
|
21846
|
+
return spawn(process.execPath, spawnArgs, {
|
|
21388
21847
|
stdio: ["ignore", "ignore", "pipe"],
|
|
21389
21848
|
env: process.env
|
|
21390
21849
|
});
|
|
21391
21850
|
}
|
|
21392
21851
|
function runCommand() {
|
|
21393
|
-
return new
|
|
21852
|
+
return new Command18("run").alias("with").description(
|
|
21394
21853
|
"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
21854
|
).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
21855
|
let provider;
|
|
21397
21856
|
try {
|
|
21398
21857
|
provider = resolveProvider(tool, opts.provider);
|
|
21399
21858
|
} catch (err) {
|
|
21400
|
-
console.error(
|
|
21859
|
+
console.error(chalk18.red(`
|
|
21401
21860
|
x ${err.message}
|
|
21402
21861
|
`));
|
|
21403
21862
|
process.exit(1);
|
|
@@ -21408,13 +21867,13 @@ function runCommand() {
|
|
|
21408
21867
|
const rec = readProxyRecord();
|
|
21409
21868
|
if (rec && isProcessAlive(rec.pid) && await isProxyHealthy(rec.port)) {
|
|
21410
21869
|
port = rec.port;
|
|
21411
|
-
console.error(
|
|
21870
|
+
console.error(chalk18.dim(` \u21BA Reusing the Pretense proxy already running on port ${port}.`));
|
|
21412
21871
|
} else {
|
|
21413
21872
|
const preferred = opts.port ? parseInt(opts.port, 10) : 9339;
|
|
21414
21873
|
try {
|
|
21415
21874
|
port = await findFreePort(preferred);
|
|
21416
21875
|
} catch (err) {
|
|
21417
|
-
console.error(
|
|
21876
|
+
console.error(chalk18.red(`
|
|
21418
21877
|
x ${err.message}
|
|
21419
21878
|
`));
|
|
21420
21879
|
process.exit(1);
|
|
@@ -21428,14 +21887,14 @@ function runCommand() {
|
|
|
21428
21887
|
const ready = await waitHealthy(port, 15e3);
|
|
21429
21888
|
if (!ready) {
|
|
21430
21889
|
console.error(
|
|
21431
|
-
|
|
21432
|
-
x Pretense proxy did not become healthy on port ${port} in time.`) + (proxyStderr ?
|
|
21890
|
+
chalk18.red(`
|
|
21891
|
+
x Pretense proxy did not become healthy on port ${port} in time.`) + (proxyStderr ? chalk18.dim(`
|
|
21433
21892
|
${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
|
|
21434
21893
|
);
|
|
21435
21894
|
proxyChild.kill("SIGTERM");
|
|
21436
21895
|
process.exit(1);
|
|
21437
21896
|
}
|
|
21438
|
-
console.error(
|
|
21897
|
+
console.error(chalk18.green(` \u2713 Started Pretense proxy on port ${port}.`));
|
|
21439
21898
|
}
|
|
21440
21899
|
const stopStartedProxy = () => {
|
|
21441
21900
|
if (startedByUs && proxyChild && !proxyChild.killed) {
|
|
@@ -21444,7 +21903,7 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
|
|
|
21444
21903
|
};
|
|
21445
21904
|
const inject = injectedEnvFor(provider, port);
|
|
21446
21905
|
console.error(
|
|
21447
|
-
|
|
21906
|
+
chalk18.dim(
|
|
21448
21907
|
` \u2192 Routing ${basename3(tool)} through the proxy (${primaryEnvVar(provider)}=http://localhost:${port}).
|
|
21449
21908
|
`
|
|
21450
21909
|
)
|
|
@@ -21463,15 +21922,15 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
|
|
|
21463
21922
|
child.on("error", (err) => {
|
|
21464
21923
|
if (err.code === "ENOENT") {
|
|
21465
21924
|
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:`) +
|
|
21925
|
+
chalk18.red(`
|
|
21926
|
+
x "${tool}" is not on your PATH.`) + chalk18.dim(`
|
|
21927
|
+
Install it (e.g. the ${basename3(tool)} CLI) and try again, or check the name.`) + chalk18.dim(`
|
|
21928
|
+
You can still point a client at the proxy yourself:`) + chalk18.dim(`
|
|
21470
21929
|
${primaryEnvVar(provider)}=http://localhost:${port}
|
|
21471
21930
|
`)
|
|
21472
21931
|
);
|
|
21473
21932
|
} else {
|
|
21474
|
-
console.error(
|
|
21933
|
+
console.error(chalk18.red(`
|
|
21475
21934
|
x Could not run "${tool}": ${err.message}
|
|
21476
21935
|
`));
|
|
21477
21936
|
}
|
|
@@ -21490,8 +21949,8 @@ ${proxyStderr.split("\n").slice(-6).join("\n")}`) : "") + "\n"
|
|
|
21490
21949
|
|
|
21491
21950
|
// src/commands/policy.ts
|
|
21492
21951
|
init_esm_shims();
|
|
21493
|
-
import { Command as
|
|
21494
|
-
import
|
|
21952
|
+
import { Command as Command19 } from "commander";
|
|
21953
|
+
import chalk19 from "chalk";
|
|
21495
21954
|
function listAction(opts) {
|
|
21496
21955
|
const ids = listPresetIds();
|
|
21497
21956
|
if (opts.json) {
|
|
@@ -21514,21 +21973,21 @@ function listAction(opts) {
|
|
|
21514
21973
|
);
|
|
21515
21974
|
return;
|
|
21516
21975
|
}
|
|
21517
|
-
console.log(
|
|
21976
|
+
console.log(chalk19.cyan("\n Pretense Compliance Frameworks\n"));
|
|
21518
21977
|
console.log(
|
|
21519
|
-
|
|
21978
|
+
chalk19.dim(
|
|
21520
21979
|
" " + "ID".padEnd(8) + "NAME".padEnd(14) + "TIER".padEnd(12) + "AUDIT".padEnd(8) + "DETECTORS"
|
|
21521
21980
|
)
|
|
21522
21981
|
);
|
|
21523
|
-
console.log(
|
|
21982
|
+
console.log(chalk19.dim(" " + "\u2500".repeat(64)));
|
|
21524
21983
|
for (const id of ids) {
|
|
21525
21984
|
const p = COMPLIANCE_PRESETS[id];
|
|
21526
21985
|
console.log(
|
|
21527
|
-
" " +
|
|
21986
|
+
" " + 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
21987
|
);
|
|
21529
21988
|
}
|
|
21530
|
-
console.log(
|
|
21531
|
-
Run ${
|
|
21989
|
+
console.log(chalk19.dim(`
|
|
21990
|
+
Run ${chalk19.cyan("pretense policy show <id>")} for detector-level detail.
|
|
21532
21991
|
`));
|
|
21533
21992
|
}
|
|
21534
21993
|
function showAction(id, opts) {
|
|
@@ -21537,8 +21996,8 @@ function showAction(id, opts) {
|
|
|
21537
21996
|
preset = loadPreset(id);
|
|
21538
21997
|
} catch (err) {
|
|
21539
21998
|
console.error(
|
|
21540
|
-
|
|
21541
|
-
x ${err.message}`) +
|
|
21999
|
+
chalk19.red(`
|
|
22000
|
+
x ${err.message}`) + chalk19.dim(`
|
|
21542
22001
|
Valid frameworks: ${listPresetIds().join(", ")}
|
|
21543
22002
|
`)
|
|
21544
22003
|
);
|
|
@@ -21548,24 +22007,24 @@ function showAction(id, opts) {
|
|
|
21548
22007
|
console.log(JSON.stringify(preset, null, 2));
|
|
21549
22008
|
return;
|
|
21550
22009
|
}
|
|
21551
|
-
console.log(
|
|
21552
|
-
${preset.name} `) +
|
|
21553
|
-
console.log(
|
|
22010
|
+
console.log(chalk19.cyan(`
|
|
22011
|
+
${preset.name} `) + chalk19.dim(`(${preset.id})`));
|
|
22012
|
+
console.log(chalk19.dim(` ${preset.description}
|
|
21554
22013
|
`));
|
|
21555
|
-
console.log(` Minimum tier: ${
|
|
21556
|
-
console.log(` Audit required: ${preset.auditRequired ?
|
|
21557
|
-
console.log(` Detector sets: ${
|
|
22014
|
+
console.log(` Minimum tier: ${chalk19.bold(preset.minTier)}`);
|
|
22015
|
+
console.log(` Audit required: ${preset.auditRequired ? chalk19.green("yes") : chalk19.dim("no")}`);
|
|
22016
|
+
console.log(` Detector sets: ${chalk19.dim(preset.detectorSets.join(", "))}
|
|
21558
22017
|
`);
|
|
21559
22018
|
const overrides = Object.entries(preset.actionOverrides);
|
|
21560
|
-
console.log(
|
|
22019
|
+
console.log(chalk19.dim(` Detector actions (${overrides.length}):`));
|
|
21561
22020
|
for (const [detector, action] of overrides) {
|
|
21562
|
-
const color = action === "block" ?
|
|
22021
|
+
const color = action === "block" ? chalk19.red : action === "redact" ? chalk19.yellow : chalk19.dim;
|
|
21563
22022
|
console.log(` ${detector.padEnd(24)} ${color(action)}`);
|
|
21564
22023
|
}
|
|
21565
22024
|
console.log();
|
|
21566
22025
|
}
|
|
21567
22026
|
function policyCommand() {
|
|
21568
|
-
const cmd = new
|
|
22027
|
+
const cmd = new Command19("policy").description(
|
|
21569
22028
|
"List and inspect the built-in compliance presets (5: hipaa, gdpr, soc2, nist, pci)"
|
|
21570
22029
|
);
|
|
21571
22030
|
cmd.command("list").description("List the 5 built-in compliance presets").option("--json", "Output as JSON").action((opts) => listAction(opts));
|
|
@@ -21576,10 +22035,10 @@ function policyCommand() {
|
|
|
21576
22035
|
|
|
21577
22036
|
// src/commands/audit.ts
|
|
21578
22037
|
init_esm_shims();
|
|
21579
|
-
import { Command as
|
|
21580
|
-
import
|
|
21581
|
-
import { homedir as
|
|
21582
|
-
import { join as
|
|
22038
|
+
import { Command as Command20 } from "commander";
|
|
22039
|
+
import chalk20 from "chalk";
|
|
22040
|
+
import { homedir as homedir11 } from "os";
|
|
22041
|
+
import { join as join24 } from "path";
|
|
21583
22042
|
var VALID_FORMATS2 = ["table", "csv", "json", "jsonl"];
|
|
21584
22043
|
function toSafeAuditRow(e) {
|
|
21585
22044
|
return {
|
|
@@ -21650,18 +22109,18 @@ function resolveFormat2(opts) {
|
|
|
21650
22109
|
const raw2 = opts.format?.trim().toLowerCase();
|
|
21651
22110
|
if (raw2 !== void 0 && !VALID_FORMATS2.includes(raw2)) {
|
|
21652
22111
|
console.error(
|
|
21653
|
-
|
|
22112
|
+
chalk20.red(`
|
|
21654
22113
|
x Unknown --format: "${opts.format}"
|
|
21655
|
-
`) +
|
|
22114
|
+
`) + chalk20.dim(" Valid formats: table, csv, json (alias: jsonl).\n")
|
|
21656
22115
|
);
|
|
21657
22116
|
return null;
|
|
21658
22117
|
}
|
|
21659
22118
|
const fromFormat = raw2 === void 0 ? void 0 : raw2 === "jsonl" ? "json" : raw2;
|
|
21660
22119
|
if (opts.json && fromFormat !== void 0 && fromFormat !== "json") {
|
|
21661
22120
|
console.error(
|
|
21662
|
-
|
|
22121
|
+
chalk20.red(`
|
|
21663
22122
|
x --json and --format ${fromFormat} request different outputs.
|
|
21664
|
-
`) +
|
|
22123
|
+
`) + chalk20.dim(" Pick one.\n")
|
|
21665
22124
|
);
|
|
21666
22125
|
return null;
|
|
21667
22126
|
}
|
|
@@ -21669,17 +22128,17 @@ function resolveFormat2(opts) {
|
|
|
21669
22128
|
return opts.json ? "json" : "table";
|
|
21670
22129
|
}
|
|
21671
22130
|
function auditCommand() {
|
|
21672
|
-
return new
|
|
22131
|
+
return new Command20("audit").description(
|
|
21673
22132
|
"Show recent mutation/tokenization audit entries (leak-proof; JSONL with --json, RFC 4180 CSV with --format csv)"
|
|
21674
22133
|
).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
22134
|
"--framework <name>",
|
|
21676
|
-
"Only entries whose detectors map to this compliance framework (e.g. HIPAA, SOC2, PCI_DSS). Use --list-frameworks to see all
|
|
22135
|
+
"Only entries whose detectors map to this compliance framework (e.g. HIPAA, SOC2, PCI_DSS). Use --list-frameworks to see them all."
|
|
21677
22136
|
).option("--list-frameworks", "List the compliance frameworks that can be filtered on", false).option(
|
|
21678
22137
|
"--format <fmt>",
|
|
21679
22138
|
"Output format: table (default), csv (RFC 4180 compliance export), json (JSONL)"
|
|
21680
22139
|
).action((opts) => {
|
|
21681
22140
|
if (opts.listFrameworks) {
|
|
21682
|
-
for (const f of
|
|
22141
|
+
for (const f of POLICY_FRAMEWORKS) console.log(f);
|
|
21683
22142
|
return;
|
|
21684
22143
|
}
|
|
21685
22144
|
const format = resolveFormat2(opts);
|
|
@@ -21689,21 +22148,21 @@ function auditCommand() {
|
|
|
21689
22148
|
framework = resolveFrameworkName(opts.framework);
|
|
21690
22149
|
if (!framework) {
|
|
21691
22150
|
console.error(
|
|
21692
|
-
|
|
22151
|
+
chalk20.red(`
|
|
21693
22152
|
x Unknown framework: "${opts.framework}"
|
|
21694
|
-
`) +
|
|
22153
|
+
`) + chalk20.dim(" Run ") + chalk20.cyan("pretense audit --list-frameworks") + chalk20.dim(" to see them all.\n")
|
|
21695
22154
|
);
|
|
21696
22155
|
process.exit(1);
|
|
21697
22156
|
}
|
|
21698
22157
|
}
|
|
21699
22158
|
const limit = parseInt(opts.limit ?? "20", 10);
|
|
21700
22159
|
if (!Number.isFinite(limit) || limit <= 0) {
|
|
21701
|
-
console.error(
|
|
22160
|
+
console.error(chalk20.red(`
|
|
21702
22161
|
x Invalid --limit: "${opts.limit}". Provide a positive integer.
|
|
21703
22162
|
`));
|
|
21704
22163
|
process.exit(1);
|
|
21705
22164
|
}
|
|
21706
|
-
const dbPath = opts.db ??
|
|
22165
|
+
const dbPath = opts.db ?? join24(homedir11(), ".pretense", "audit.db");
|
|
21707
22166
|
let rows;
|
|
21708
22167
|
try {
|
|
21709
22168
|
const store = new AuditStore(dbPath);
|
|
@@ -21723,8 +22182,8 @@ function auditCommand() {
|
|
|
21723
22182
|
console.error("pretense: no audit data \u2014 the audit database has not been initialized yet.");
|
|
21724
22183
|
return;
|
|
21725
22184
|
}
|
|
21726
|
-
console.log(
|
|
21727
|
-
console.log(
|
|
22185
|
+
console.log(chalk20.dim("\n No audit data available. The audit database has not been initialized yet."));
|
|
22186
|
+
console.log(chalk20.dim(" Run " + chalk20.cyan("pretense start") + " and make some requests first.\n"));
|
|
21728
22187
|
return;
|
|
21729
22188
|
}
|
|
21730
22189
|
if (format === "csv") {
|
|
@@ -21736,23 +22195,23 @@ function auditCommand() {
|
|
|
21736
22195
|
return;
|
|
21737
22196
|
}
|
|
21738
22197
|
if (rows.length === 0) {
|
|
21739
|
-
console.log(
|
|
22198
|
+
console.log(chalk20.dim("\n No audit entries yet. Run pretense start and make some requests.\n"));
|
|
21740
22199
|
return;
|
|
21741
22200
|
}
|
|
21742
|
-
console.log(
|
|
22201
|
+
console.log(chalk20.cyan(`
|
|
21743
22202
|
Pretense Mutation Audit \u2014 last ${rows.length} entries
|
|
21744
22203
|
`));
|
|
21745
22204
|
console.log(
|
|
21746
|
-
|
|
22205
|
+
chalk20.dim(
|
|
21747
22206
|
" " + "TIME".padEnd(10) + "ACTION".padEnd(12) + "PROVIDER".padEnd(14) + "MUTATED".padEnd(10) + "BLOCKED".padEnd(10) + "PII".padEnd(8) + "MS"
|
|
21748
22207
|
)
|
|
21749
22208
|
);
|
|
21750
|
-
console.log(
|
|
22209
|
+
console.log(chalk20.dim(" " + "\u2500".repeat(72)));
|
|
21751
22210
|
for (const row of rows) {
|
|
21752
22211
|
const time = new Date(row.timestamp).toTimeString().slice(0, 8);
|
|
21753
|
-
const actionColor = row.action === "blocked" ?
|
|
22212
|
+
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
22213
|
console.log(
|
|
21755
|
-
" " +
|
|
22214
|
+
" " + 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
22215
|
);
|
|
21757
22216
|
}
|
|
21758
22217
|
console.log();
|
|
@@ -21761,10 +22220,10 @@ function auditCommand() {
|
|
|
21761
22220
|
|
|
21762
22221
|
// src/commands/credits.ts
|
|
21763
22222
|
init_esm_shims();
|
|
21764
|
-
import { Command as
|
|
21765
|
-
import
|
|
21766
|
-
import { join as
|
|
21767
|
-
import { homedir as
|
|
22223
|
+
import { Command as Command21 } from "commander";
|
|
22224
|
+
import chalk21 from "chalk";
|
|
22225
|
+
import { join as join25 } from "path";
|
|
22226
|
+
import { homedir as homedir12 } from "os";
|
|
21768
22227
|
function remaining(used, limit) {
|
|
21769
22228
|
if (limit === null) return null;
|
|
21770
22229
|
return Math.max(0, limit - used);
|
|
@@ -21773,16 +22232,16 @@ function fmt(n) {
|
|
|
21773
22232
|
return n === null ? "unlimited" : n.toLocaleString("en-US");
|
|
21774
22233
|
}
|
|
21775
22234
|
function bar(used, limit, width = 24) {
|
|
21776
|
-
if (limit === null) return
|
|
22235
|
+
if (limit === null) return chalk21.green("\u2588".repeat(width));
|
|
21777
22236
|
const pct = limit <= 0 ? 1 : Math.min(1, used / limit);
|
|
21778
22237
|
const filled = Math.min(width, Math.round(pct * width));
|
|
21779
22238
|
const s = "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
21780
|
-
if (pct >= 1) return
|
|
21781
|
-
if (pct >= 0.8) return
|
|
21782
|
-
return
|
|
22239
|
+
if (pct >= 1) return chalk21.red(s);
|
|
22240
|
+
if (pct >= 0.8) return chalk21.yellow(s);
|
|
22241
|
+
return chalk21.green(s);
|
|
21783
22242
|
}
|
|
21784
22243
|
function creditsCommand() {
|
|
21785
|
-
return new
|
|
22244
|
+
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
22245
|
const resolution = await resolvePlanTier({ offline: opts.offline });
|
|
21787
22246
|
const tier = resolution.tier;
|
|
21788
22247
|
const collected = collectUsage(opts.db);
|
|
@@ -21795,7 +22254,7 @@ function creditsCommand() {
|
|
|
21795
22254
|
periodEnd: null
|
|
21796
22255
|
};
|
|
21797
22256
|
if (!collected && !opts.json) {
|
|
21798
|
-
console.log(
|
|
22257
|
+
console.log(chalk21.dim("\n No usage recorded yet \u2014 showing your full allowance.\n"));
|
|
21799
22258
|
}
|
|
21800
22259
|
const mutationLimit = tier ? PLAN_CONFIGS[tier].mutationsPerPeriod : null;
|
|
21801
22260
|
const scanLimit = tier ? PLAN_CONFIGS[tier].scansPerPeriod : null;
|
|
@@ -21827,12 +22286,12 @@ function creditsCommand() {
|
|
|
21827
22286
|
);
|
|
21828
22287
|
return;
|
|
21829
22288
|
}
|
|
21830
|
-
console.log(
|
|
21831
|
-
console.log(
|
|
22289
|
+
console.log(chalk21.cyan("\n Pretense Credits"));
|
|
22290
|
+
console.log(chalk21.dim(` Plan: ${tierLabel(tier)} | Resets: ${resetCadenceLabel()}
|
|
21832
22291
|
`));
|
|
21833
22292
|
if (!tier) {
|
|
21834
22293
|
for (const line of unverifiedNote(resolution.detail)) {
|
|
21835
|
-
console.log(
|
|
22294
|
+
console.log(chalk21.yellow(` ${line}`));
|
|
21836
22295
|
}
|
|
21837
22296
|
console.log();
|
|
21838
22297
|
console.log(" " + "Mutations used".padEnd(20) + m.mutationsApplied.toLocaleString("en-US"));
|
|
@@ -21841,18 +22300,18 @@ function creditsCommand() {
|
|
|
21841
22300
|
return;
|
|
21842
22301
|
}
|
|
21843
22302
|
console.log(
|
|
21844
|
-
" " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " +
|
|
22303
|
+
" " + "Mutations".padEnd(14) + bar(m.mutationsApplied, mutationLimit) + " " + chalk21.bold(fmt(mutationsLeft)) + chalk21.dim(` left of ${fmt(mutationLimit)}`)
|
|
21845
22304
|
);
|
|
21846
22305
|
console.log(
|
|
21847
|
-
" " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " +
|
|
22306
|
+
" " + "Requests".padEnd(14) + bar(m.totalScans, scanLimit) + " " + chalk21.bold(fmt(scansLeft)) + chalk21.dim(` left of ${fmt(scanLimit)}`)
|
|
21848
22307
|
);
|
|
21849
22308
|
if (tier !== "enterprise" && mutationLimit !== null && mutationsLeft === 0) {
|
|
21850
22309
|
console.log(
|
|
21851
|
-
|
|
22310
|
+
chalk21.red("\n Mutation budget exhausted for this period.") + chalk21.dim(" Run ") + chalk21.cyan("pretense upgrade") + chalk21.dim(" to compare plans.")
|
|
21852
22311
|
);
|
|
21853
22312
|
} else if (tier !== "enterprise" && mutationLimit !== null && m.mutationsApplied >= mutationLimit * 0.8) {
|
|
21854
22313
|
console.log(
|
|
21855
|
-
|
|
22314
|
+
chalk21.yellow("\n Running low.") + chalk21.dim(" Run ") + chalk21.cyan("pretense upgrade") + chalk21.dim(" to compare plans.")
|
|
21856
22315
|
);
|
|
21857
22316
|
}
|
|
21858
22317
|
console.log();
|
|
@@ -21861,21 +22320,24 @@ function creditsCommand() {
|
|
|
21861
22320
|
|
|
21862
22321
|
// src/commands/upgrade.ts
|
|
21863
22322
|
init_esm_shims();
|
|
21864
|
-
import { Command as
|
|
21865
|
-
import
|
|
22323
|
+
import { Command as Command22 } from "commander";
|
|
22324
|
+
import chalk22 from "chalk";
|
|
21866
22325
|
var PRICING_URL = "https://pretense.ai/pricing";
|
|
21867
22326
|
var TIERS = ["free", "pro", "enterprise"];
|
|
21868
22327
|
function quota(n) {
|
|
21869
22328
|
return n === null ? "unlimited" : n.toLocaleString();
|
|
21870
22329
|
}
|
|
21871
22330
|
function seats(n) {
|
|
21872
|
-
return n === null ? "
|
|
22331
|
+
return n === null ? "up to 100" : String(n);
|
|
22332
|
+
}
|
|
22333
|
+
function comingSoonOrDash(enabled) {
|
|
22334
|
+
return enabled ? "coming soon" : "\u2014";
|
|
21873
22335
|
}
|
|
21874
22336
|
function yesNo(b) {
|
|
21875
22337
|
return b ? "yes" : "\u2014";
|
|
21876
22338
|
}
|
|
21877
22339
|
function upgradeCommand() {
|
|
21878
|
-
return new
|
|
22340
|
+
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
22341
|
const resolution = await resolvePlanTier({ offline: opts.offline });
|
|
21880
22342
|
const current = resolution.tier;
|
|
21881
22343
|
if (opts.json) {
|
|
@@ -21903,40 +22365,41 @@ function upgradeCommand() {
|
|
|
21903
22365
|
);
|
|
21904
22366
|
return;
|
|
21905
22367
|
}
|
|
21906
|
-
console.log(
|
|
22368
|
+
console.log(chalk22.cyan("\n Pretense Plans\n"));
|
|
21907
22369
|
const col = 16;
|
|
21908
22370
|
const header = " " + "".padEnd(18) + TIERS.map((t) => tierName(t).padEnd(col)).join("");
|
|
21909
|
-
console.log(
|
|
21910
|
-
console.log(" " +
|
|
22371
|
+
console.log(chalk22.bold(header));
|
|
22372
|
+
console.log(" " + chalk22.dim("\u2500".repeat(18 + col * TIERS.length)));
|
|
21911
22373
|
const rows = [
|
|
21912
22374
|
["Price", (t) => priceLabel(t)],
|
|
21913
22375
|
["Mutations/7d", (t) => quota(PLAN_CONFIGS[t].mutationsPerPeriod)],
|
|
21914
22376
|
["Requests/7d", (t) => quota(PLAN_CONFIGS[t].scansPerPeriod)],
|
|
21915
22377
|
["Seats", (t) => seats(PLAN_CONFIGS[t].maxSeats)],
|
|
21916
|
-
["SSO", (t) =>
|
|
22378
|
+
["SSO", (t) => comingSoonOrDash(PLAN_CONFIGS[t].ssoEnabled)],
|
|
22379
|
+
// On-prem IS truthful — Docker + Helm ship — so it stays yes/—.
|
|
21917
22380
|
["On-prem", (t) => yesNo(PLAN_CONFIGS[t].onPremEnabled)]
|
|
21918
22381
|
];
|
|
21919
22382
|
for (const [label, cell] of rows) {
|
|
21920
22383
|
console.log(" " + label.padEnd(18) + TIERS.map((t) => cell(t).padEnd(col)).join(""));
|
|
21921
22384
|
}
|
|
21922
|
-
console.log(" " +
|
|
22385
|
+
console.log(" " + chalk22.dim("\u2500".repeat(18 + col * TIERS.length)));
|
|
21923
22386
|
if (current) {
|
|
21924
|
-
console.log(
|
|
22387
|
+
console.log(chalk22.dim(`
|
|
21925
22388
|
Current plan: ${tierName(current)}`));
|
|
21926
22389
|
if (current === "enterprise") {
|
|
21927
|
-
console.log(
|
|
22390
|
+
console.log(chalk22.green(" You are on the highest tier \u2014 nothing to upgrade.\n"));
|
|
21928
22391
|
return;
|
|
21929
22392
|
}
|
|
21930
22393
|
} else {
|
|
21931
22394
|
console.log(
|
|
21932
|
-
|
|
22395
|
+
chalk22.yellow(
|
|
21933
22396
|
`
|
|
21934
22397
|
Could not confirm your current plan: ${resolution.detail ?? "unknown reason"}.`
|
|
21935
22398
|
)
|
|
21936
22399
|
);
|
|
21937
22400
|
}
|
|
21938
22401
|
const from = current ?? "unknown";
|
|
21939
|
-
console.log(
|
|
22402
|
+
console.log(chalk22.bold(`
|
|
21940
22403
|
\u2192 ${PRICING_URL}?ref=cli_upgrade&from=${from}
|
|
21941
22404
|
`));
|
|
21942
22405
|
});
|
|
@@ -21969,35 +22432,36 @@ function findClosestCommand(input, knownCommands) {
|
|
|
21969
22432
|
}
|
|
21970
22433
|
return bestDist <= 3 ? best : null;
|
|
21971
22434
|
}
|
|
21972
|
-
var program = new
|
|
21973
|
-
program.name("pretense").enablePositionalOptions().description(
|
|
21974
|
-
${
|
|
21975
|
-
${
|
|
21976
|
-
${
|
|
21977
|
-
${
|
|
22435
|
+
var program = new Command23();
|
|
22436
|
+
program.name("pretense").enablePositionalOptions().description(chalk23.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version(PRETENSE_VERSION, "-v, --version").addHelpText("after", `
|
|
22437
|
+
${chalk23.bold("Quick start:")}
|
|
22438
|
+
${chalk23.cyan("pretense run claude")} ${chalk23.dim('"build X"')} Run an AI tool through the proxy (zero setup)
|
|
22439
|
+
${chalk23.cyan("pretense mutate")} ${chalk23.dim("<file>")} Mutate secrets in place (reverse with the same path)
|
|
22440
|
+
${chalk23.cyan("pretense scan")} ${chalk23.dim("<file|dir>")} Scan for secrets and PII
|
|
21978
22441
|
|
|
21979
|
-
${
|
|
21980
|
-
${
|
|
21981
|
-
${
|
|
21982
|
-
${
|
|
21983
|
-
${
|
|
21984
|
-
${
|
|
21985
|
-
${
|
|
21986
|
-
${
|
|
21987
|
-
${
|
|
21988
|
-
${
|
|
21989
|
-
${
|
|
21990
|
-
${
|
|
21991
|
-
${
|
|
21992
|
-
${
|
|
21993
|
-
${
|
|
21994
|
-
${
|
|
21995
|
-
${
|
|
21996
|
-
${
|
|
21997
|
-
${
|
|
21998
|
-
${
|
|
22442
|
+
${chalk23.dim("All commands:")}
|
|
22443
|
+
${chalk23.cyan("pretense init")} Scan codebase and build protection profile
|
|
22444
|
+
${chalk23.cyan("pretense run <tool>")} Run an AI tool through the proxy, no manual export
|
|
22445
|
+
${chalk23.cyan("pretense start")} Start proxy on localhost:9339
|
|
22446
|
+
${chalk23.cyan("pretense stop")} Stop the running proxy
|
|
22447
|
+
${chalk23.cyan("pretense scan src/api.ts")} Scan file for secrets
|
|
22448
|
+
${chalk23.cyan("pretense scan ci")} CI-optimized scan (exit 0=clean, 2=findings)
|
|
22449
|
+
${chalk23.cyan("pretense status")} Show protection status
|
|
22450
|
+
${chalk23.cyan("pretense doctor")} Diagnose why the proxy isn't catching traffic
|
|
22451
|
+
${chalk23.cyan("pretense review")} Preview mutations that would be applied
|
|
22452
|
+
${chalk23.cyan("pretense review --json")} Output review as JSON
|
|
22453
|
+
${chalk23.cyan("pretense mutate <file>")} Mutate secrets in place + auto-write reversal map
|
|
22454
|
+
${chalk23.cyan("pretense reverse <file>")} Restore a mutated file (auto-finds its map)
|
|
22455
|
+
${chalk23.cyan("pretense policy list")} List the 5 compliance presets
|
|
22456
|
+
${chalk23.cyan("pretense audit")} Show mutation audit log
|
|
22457
|
+
${chalk23.cyan("pretense usage")} Plan usage vs limits
|
|
22458
|
+
${chalk23.cyan("pretense credits")} Remaining mutation budget (alias: tokens)
|
|
22459
|
+
${chalk23.cyan("pretense upgrade")} Compare plans / upgrade
|
|
22460
|
+
${chalk23.cyan("pretense auth login")} Authenticate with Pretense
|
|
22461
|
+
${chalk23.cyan("pretense install")} Install git hooks
|
|
22462
|
+
${chalk23.cyan("pretense logs --limit 20")} Show recent audit entries
|
|
21999
22463
|
|
|
22000
|
-
${
|
|
22464
|
+
${chalk23.dim("Run")} ${chalk23.cyan("pretense <command> --help")} ${chalk23.dim("for command-specific help.")}
|
|
22001
22465
|
`);
|
|
22002
22466
|
var KNOWN_COMMANDS = [
|
|
22003
22467
|
"init",
|
|
@@ -22025,7 +22489,8 @@ var KNOWN_COMMANDS = [
|
|
|
22025
22489
|
"logout",
|
|
22026
22490
|
"credits",
|
|
22027
22491
|
"tokens",
|
|
22028
|
-
"upgrade"
|
|
22492
|
+
"upgrade",
|
|
22493
|
+
"doctor"
|
|
22029
22494
|
];
|
|
22030
22495
|
program.command("version").description("Print Pretense CLI version").action(() => {
|
|
22031
22496
|
console.log(`@pretense/cli v${program.version()}`);
|
|
@@ -22058,16 +22523,17 @@ program.addCommand(policyCommand());
|
|
|
22058
22523
|
program.addCommand(auditCommand());
|
|
22059
22524
|
program.addCommand(creditsCommand());
|
|
22060
22525
|
program.addCommand(upgradeCommand());
|
|
22526
|
+
program.addCommand(doctorCommand());
|
|
22061
22527
|
program.on("command:*", (operands) => {
|
|
22062
22528
|
const unknown = operands[0] ?? "";
|
|
22063
22529
|
const suggestion = findClosestCommand(unknown, KNOWN_COMMANDS);
|
|
22064
|
-
console.error(
|
|
22530
|
+
console.error(chalk23.red(`
|
|
22065
22531
|
x Unknown command: "${unknown}"`));
|
|
22066
22532
|
if (suggestion) {
|
|
22067
|
-
console.error(
|
|
22533
|
+
console.error(chalk23.yellow(` Did you mean: ${chalk23.bold(`pretense ${suggestion}`)}`));
|
|
22068
22534
|
}
|
|
22069
|
-
console.error(
|
|
22070
|
-
Run ${
|
|
22535
|
+
console.error(chalk23.dim(`
|
|
22536
|
+
Run ${chalk23.cyan("pretense --help")} to see all available commands.
|
|
22071
22537
|
`));
|
|
22072
22538
|
process.exit(1);
|
|
22073
22539
|
});
|