@pretense/cli 0.6.4 → 0.6.5

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.
File without changes
File without changes
package/dist/index.js CHANGED
@@ -10385,7 +10385,6 @@ function listPresetIds() {
10385
10385
 
10386
10386
  // ../packages/scanner/dist/index.js
10387
10387
  import { gunzipSync } from "zlib";
10388
- import { createRequire } from "module";
10389
10388
  var SECRET_LABEL_TAILS = [
10390
10389
  // password family
10391
10390
  "password",
@@ -13279,259 +13278,6 @@ function resolveSafeSpan(original, map, viewStart, viewEnd, value, cost) {
13279
13278
  if (!spanContainsValue(slice, value, cost)) return null;
13280
13279
  return { start, end, value: slice };
13281
13280
  }
13282
- var _native;
13283
- function loadNative() {
13284
- if (_native !== void 0) return _native;
13285
- try {
13286
- const require2 = createRequire(import.meta.url);
13287
- const mod = require2("@pretense/scanner-rs");
13288
- if (typeof mod?.semanticScan !== "function") {
13289
- _native = null;
13290
- } else {
13291
- _native = mod;
13292
- }
13293
- } catch {
13294
- _native = null;
13295
- }
13296
- return _native;
13297
- }
13298
- var _astProbe;
13299
- function isAstCoreAvailable() {
13300
- if (_astProbe !== void 0) return _astProbe;
13301
- const native = loadNative();
13302
- if (!native) {
13303
- _astProbe = false;
13304
- return false;
13305
- }
13306
- try {
13307
- native.semanticScan("", "typescript");
13308
- _astProbe = true;
13309
- } catch {
13310
- _astProbe = false;
13311
- }
13312
- return _astProbe;
13313
- }
13314
- function makeByteToChar(text) {
13315
- let ascii = true;
13316
- for (let i = 0; i < text.length; i++) {
13317
- if (text.charCodeAt(i) > 127) {
13318
- ascii = false;
13319
- break;
13320
- }
13321
- }
13322
- if (ascii) return (b) => b;
13323
- const totalBytes = Buffer.byteLength(text, "utf8");
13324
- const table = new Int32Array(totalBytes + 1);
13325
- let byte = 0;
13326
- for (let charIdx = 0; charIdx < text.length; ) {
13327
- const cp = text.codePointAt(charIdx);
13328
- const unitLen = cp > 65535 ? 2 : 1;
13329
- const byteLen = Buffer.byteLength(String.fromCodePoint(cp), "utf8");
13330
- for (let k = 0; k < byteLen; k++) table[byte + k] = charIdx;
13331
- byte += byteLen;
13332
- charIdx += unitLen;
13333
- }
13334
- table[totalBytes] = text.length;
13335
- return (b) => {
13336
- if (b <= 0) return 0;
13337
- if (b >= totalBytes) return text.length;
13338
- return table[b];
13339
- };
13340
- }
13341
- function isInsideAnySpan(start, end, spans) {
13342
- for (const s of spans) {
13343
- if (s.start > start) break;
13344
- if (start >= s.start && end <= s.end) return true;
13345
- }
13346
- return false;
13347
- }
13348
- var CREDENTIAL_NAME = /(?:^|[._-]|(?<=[a-z0-9]))(?:api[_-]?key|api[_-]?secret|secret|secrets|token|password|passwd|pwd|access[_-]?key|access[_-]?token|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer[_-]?token|session[_-]?secret|encryption[_-]?key|signing[_-]?key)s?$/i;
13349
- var CREDENTIAL_SETTER = /^(?:set|with|configure|use|update|put|load|init)[_-]?(?:api[_-]?key|api[_-]?secret|secret|token|password|passwd|pwd|access[_-]?key|access[_-]?token|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer[_-]?token|credential|credentials|key)s?$/i;
13350
- function looksLikeCredentialName(name) {
13351
- return CREDENTIAL_NAME.test(name);
13352
- }
13353
- function looksLikeCredentialSetter(callee) {
13354
- const last = callee.split(".").pop() ?? callee;
13355
- return CREDENTIAL_SETTER.test(last);
13356
- }
13357
- function unquoteLiteral(literal) {
13358
- const t = literal.trim();
13359
- if (t.length < 2) return null;
13360
- const open = t[0];
13361
- const close = t[t.length - 1];
13362
- if (open === '"' && close === '"' || open === "'" && close === "'" || open === "`" && close === "`") {
13363
- return t.slice(1, -1);
13364
- }
13365
- const firstQuote = t.search(/["'`]/);
13366
- if (firstQuote >= 0) {
13367
- const q = t[firstQuote];
13368
- const lastQuote = t.lastIndexOf(q);
13369
- if (lastQuote > firstQuote) return t.slice(firstQuote + 1, lastQuote);
13370
- }
13371
- return null;
13372
- }
13373
- var STRING_LITERAL_RX = /^\s*["'`]/;
13374
- function dataFlowMatches(text, sem, nextId2) {
13375
- const matches = [];
13376
- const findings = [];
13377
- for (const edge of sem.edges) {
13378
- const isAssign = edge.kind === "Assignment" && looksLikeCredentialName(edge.from);
13379
- const isSetter = edge.kind === "Call" && looksLikeCredentialSetter(edge.from);
13380
- if (!isAssign && !isSetter) continue;
13381
- if (!STRING_LITERAL_RX.test(edge.to)) continue;
13382
- const inner = unquoteLiteral(edge.to);
13383
- if (inner === null || inner.length === 0) continue;
13384
- const span = locateLiteral(text, edge.to, edge.line);
13385
- const start = span && span.start >= 0 ? span.start : 0;
13386
- const end = span && span.end >= 0 ? span.end : 0;
13387
- const severity = "high";
13388
- const category = "secret";
13389
- const action = "redact";
13390
- const type = "dataflow-credential";
13391
- const id = nextId2();
13392
- const masked = maskCredential(inner);
13393
- matches.push({ id, category, type, value: inner, masked, start, end, severity, action, confidence: 0.85 });
13394
- findings.push({
13395
- ruleId: type,
13396
- value: inner,
13397
- masked,
13398
- severity,
13399
- category,
13400
- line: edge.line,
13401
- column: span?.column ?? 0,
13402
- start,
13403
- end,
13404
- action,
13405
- confidence: 0.85
13406
- });
13407
- }
13408
- return { matches, findings };
13409
- }
13410
- function maskCredential(value) {
13411
- if (value.length <= 8) return "***";
13412
- return value.slice(0, 4) + "..." + value.slice(-4);
13413
- }
13414
- function locateLiteral(text, literalSource, line) {
13415
- let lineStart = 0;
13416
- let curLine = 1;
13417
- for (let i = 0; i < text.length && curLine < line; i++) {
13418
- if (text.charCodeAt(i) === 10) {
13419
- curLine++;
13420
- lineStart = i + 1;
13421
- }
13422
- }
13423
- let idx = text.indexOf(literalSource, lineStart);
13424
- if (idx < 0) idx = text.indexOf(literalSource);
13425
- if (idx < 0) return null;
13426
- const lineNlBefore = text.lastIndexOf("\n", idx);
13427
- const column = lineNlBefore < 0 ? idx : idx - lineNlBefore - 1;
13428
- return { start: idx, end: idx + literalSource.length, column };
13429
- }
13430
- var SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1 };
13431
- function rebuildFindings(matches, findingsByMatchId) {
13432
- const out = [];
13433
- for (const m of matches) {
13434
- const f = findingsByMatchId.get(m.id);
13435
- if (f) out.push(f);
13436
- }
13437
- return out;
13438
- }
13439
- function dedupeMatches(matches) {
13440
- const sorted = [...matches].sort(
13441
- (a, b) => a.start - b.start || SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity] || b.confidence - a.confidence
13442
- );
13443
- const kept = [];
13444
- for (const m of sorted) {
13445
- const overlap = kept.find(
13446
- (k) => m.start < k.end && k.start < m.end && !(m.start === 0 && m.end === 0)
13447
- );
13448
- if (!overlap) {
13449
- kept.push(m);
13450
- }
13451
- }
13452
- const seenZero = /* @__PURE__ */ new Set();
13453
- return kept.filter((m) => {
13454
- if (m.start === 0 && m.end === 0) {
13455
- const key = `${m.type}:${m.value}`;
13456
- if (seenZero.has(key)) return false;
13457
- seenZero.add(key);
13458
- }
13459
- return true;
13460
- });
13461
- }
13462
- async function scanLevel2(text, language, opts = {}) {
13463
- const lang = language ?? opts.language ?? "typescript";
13464
- const base = opts.presidioUrl ? await scanWithPresidio(text, opts.presidioUrl, opts) : scan(text, opts);
13465
- const native = loadNative();
13466
- if (!native) return base;
13467
- let sem;
13468
- try {
13469
- sem = native.semanticScan(text, lang);
13470
- } catch {
13471
- return base;
13472
- }
13473
- const byteToChar = makeByteToChar(text);
13474
- const commentSpans = [];
13475
- for (const node of sem.nodes) {
13476
- if (node.class === "Comment") {
13477
- commentSpans.push({ start: byteToChar(node.start_byte), end: byteToChar(node.end_byte) });
13478
- }
13479
- }
13480
- commentSpans.sort((a, b) => a.start - b.start || a.end - b.end);
13481
- const findingsById = /* @__PURE__ */ new Map();
13482
- base.matches.forEach((m, i) => findingsById.set(m.id, base.findings[i] ?? toFinding(text, m)));
13483
- const survivingMatches = base.matches.filter(
13484
- (m) => m.start === m.end || !isInsideAnySpan(m.start, m.end, commentSpans)
13485
- );
13486
- let dfMatches = [];
13487
- if (opts.dataFlow !== false) {
13488
- let counter = 0;
13489
- const nextId2 = () => `l2df_${++counter}`;
13490
- const df = dataFlowMatches(text, sem, nextId2);
13491
- df.matches.forEach((m, i) => findingsById.set(m.id, df.findings[i] ?? toFinding(text, m)));
13492
- dfMatches = df.matches.filter((m) => !isInsideAnySpan(m.start, m.end, commentSpans));
13493
- }
13494
- const deduped = dedupeMatches([...survivingMatches, ...dfMatches]).sort(
13495
- (a, b) => a.start - b.start || SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]
13496
- );
13497
- const dedupedFindings = rebuildFindings(deduped, findingsById).sort(
13498
- (a, b) => a.start - b.start || SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]
13499
- );
13500
- const clean = deduped.filter((m) => m.action === "block" || m.action === "redact").length === 0;
13501
- return {
13502
- clean,
13503
- matches: deduped,
13504
- findings: dedupedFindings,
13505
- scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
13506
- durationMs: base.durationMs,
13507
- patternCount: base.patternCount,
13508
- // Bounded-work state is a property of the underlying scan and MUST survive
13509
- // this merge — dropping it here would re-create the silent-incomplete hole
13510
- // one layer up.
13511
- incomplete: base.incomplete,
13512
- limits: base.limits
13513
- };
13514
- }
13515
- function toFinding(text, m) {
13516
- const before = text.slice(0, m.start);
13517
- const line = (before.match(/\n/g) ?? []).length + 1;
13518
- const lastNl = before.lastIndexOf("\n");
13519
- const column = lastNl === -1 ? m.start : m.start - lastNl - 1;
13520
- return {
13521
- ruleId: m.type,
13522
- value: m.value,
13523
- masked: m.masked,
13524
- severity: m.severity,
13525
- category: m.category,
13526
- line,
13527
- column,
13528
- start: m.start,
13529
- end: m.end,
13530
- action: m.action,
13531
- confidence: m.confidence,
13532
- ...m.entropy !== void 0 ? { entropy: m.entropy } : {}
13533
- };
13534
- }
13535
13281
  var MIN_SPAN_VERIFY_CHARS = 1024 * 1024;
13536
13282
  var MAX_SPAN_VERIFY_CHARS = 256 * 1024 * 1024;
13537
13283
  var SPAN_VERIFY_CHARS_PER_CHAR = 64;
@@ -13878,60 +13624,6 @@ function scan(text, opts = {}) {
13878
13624
  limits
13879
13625
  };
13880
13626
  }
13881
- async function scanWithPresidio(text, presidioUrl, opts = {}) {
13882
- const { presidio: _presidio, ...syncOpts } = opts;
13883
- const base = scan(text, syncOpts);
13884
- const nlIndex = buildNewlineIndex(text);
13885
- try {
13886
- const resp = await fetch(`${presidioUrl}/analyze`, {
13887
- method: "POST",
13888
- headers: { "Content-Type": "application/json" },
13889
- body: JSON.stringify({ text, language: "en" }),
13890
- signal: AbortSignal.timeout(2e3)
13891
- });
13892
- if (!resp.ok) return base;
13893
- const presidioResults = await resp.json();
13894
- for (const pr of presidioResults) {
13895
- const alreadyCovered = base.matches.some((m) => m.start <= pr.start && m.end >= pr.end);
13896
- if (!alreadyCovered && pr.score >= 0.7) {
13897
- const value = text.slice(pr.start, pr.end);
13898
- const type = `presidio-${pr.entity_type.toLowerCase()}`;
13899
- const severity = pr.score >= 0.9 ? "high" : "medium";
13900
- base.matches.push({
13901
- id: nextId(),
13902
- category: "pii",
13903
- type,
13904
- value,
13905
- masked: "***",
13906
- start: pr.start,
13907
- end: pr.end,
13908
- severity,
13909
- action: "redact",
13910
- confidence: pr.score
13911
- });
13912
- const { line, column } = lineColFromOffset(text, pr.start, nlIndex);
13913
- base.findings.push({
13914
- ruleId: type,
13915
- value,
13916
- masked: "***",
13917
- severity,
13918
- category: "pii",
13919
- line,
13920
- column,
13921
- start: pr.start,
13922
- end: pr.end,
13923
- action: "redact",
13924
- confidence: pr.score
13925
- });
13926
- }
13927
- }
13928
- base.matches.sort((a, b) => a.start - b.start);
13929
- base.findings.sort((a, b) => a.start - b.start);
13930
- base.clean = base.matches.filter((m) => m.action === "block" || m.action === "redact").length === 0;
13931
- } catch {
13932
- }
13933
- return base;
13934
- }
13935
13627
 
13936
13628
  // ../packages/mutator/dist/index.js
13937
13629
  init_esm_shims();
@@ -17028,71 +16720,13 @@ function filterExisting(files) {
17028
16720
  }
17029
16721
  });
17030
16722
  }
17031
- function langForFile(filePath) {
17032
- switch (filePath ? extname2(filePath).toLowerCase() : "") {
17033
- case ".py":
17034
- return "python";
17035
- case ".go":
17036
- return "go";
17037
- case ".java":
17038
- return "java";
17039
- case ".rb":
17040
- return "ruby";
17041
- case ".rs":
17042
- return "rust";
17043
- case ".cs":
17044
- return "csharp";
17045
- case ".js":
17046
- case ".jsx":
17047
- return "javascript";
17048
- case ".tsx":
17049
- case ".ts":
17050
- return "typescript";
17051
- default:
17052
- return "typescript";
17053
- }
17054
- }
17055
- function resolveScanLevel(opts) {
17056
- if (opts.ast) return 2;
17057
- const raw2 = opts.level;
17058
- if (raw2 === void 0 || raw2 === "1") return 1;
17059
- if (raw2 === "2") return 2;
17060
- console.error(chalk3.red(`
17061
- x Invalid --level: "${raw2}". Use 1 (regex) or 2 (AST/data-flow).
17062
- `));
17063
- process.exit(1);
17064
- }
17065
- var _astFallbackNoticePrinted = false;
17066
- function resolveLevelWithAst(opts) {
17067
- const level = resolveScanLevel(opts);
17068
- if (level === 1) return { level, astUsed: false };
17069
- const astUsed = isAstCoreAvailable();
17070
- if (!astUsed && !_astFallbackNoticePrinted) {
17071
- console.error(
17072
- chalk3.yellow("\n ! AST core (@pretense/scanner-rs) unavailable \u2014 falling back to L1 (regex).") + chalk3.dim("\n Build the native core to enable L2 data-flow detection.\n")
17073
- );
17074
- _astFallbackNoticePrinted = true;
17075
- }
17076
- return { level, astUsed };
17077
- }
17078
- function effectiveScanLevel(requested, astUsed) {
17079
- return requested === 2 && astUsed ? 2 : 1;
17080
- }
17081
- function levelLabel(level, astUsed) {
17082
- if (level === 1) return "L1 \xB7 regex";
17083
- return astUsed ? "L2 \xB7 AST + regex" : "L2 \u2192 L1 (AST unavailable)";
17084
- }
17085
16723
  async function scanContent(content, opts, filePath) {
17086
- const base = {
16724
+ return scan(content, {
17087
16725
  contextAware: true,
17088
16726
  entropyAnalysis: opts.entropy !== false,
17089
16727
  ...filePath ? { filePath } : {},
17090
16728
  ...opts.presetName ? { presetName: opts.presetName } : {}
17091
- };
17092
- if (resolveScanLevel(opts) === 2) {
17093
- return await scanLevel2(content, langForFile(filePath), base);
17094
- }
17095
- return scan(content, base);
16729
+ });
17096
16730
  }
17097
16731
  async function scanFile(filePath, opts) {
17098
16732
  const content = readFileSync5(filePath, "utf-8");
@@ -17183,7 +16817,6 @@ function resolveFormat(opts, defaultSarif = false) {
17183
16817
  return "text";
17184
16818
  }
17185
16819
  async function outputMultiFileResults(label, files, opts, { defaultSarif = false, ciMode = false } = {}) {
17186
- const { level, astUsed } = resolveLevelWithAst(opts);
17187
16820
  const sourceFiles = files.filter(isSupportedFile);
17188
16821
  if (sourceFiles.length === 0) {
17189
16822
  const fmt3 = resolveFormat(opts, defaultSarif);
@@ -17231,7 +16864,7 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
17231
16864
  console.log(chalk3.cyan(`
17232
16865
  Pretense Scan \u2014 ${label}
17233
16866
  `));
17234
- console.log(chalk3.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()} | Level: ${levelLabel(level, astUsed)}
16867
+ console.log(chalk3.dim(` Files: ${sourceFiles.length} | Patterns: ${getPatternCount()}
17235
16868
  `));
17236
16869
  for (const { file, result } of allResults) printFindings(file, result);
17237
16870
  console.log(chalk3.dim(`
@@ -17240,7 +16873,7 @@ async function outputMultiFileResults(label, files, opts, { defaultSarif = false
17240
16873
  process.exit(findingsExitCode);
17241
16874
  }
17242
16875
  function addScanOpts(cmd) {
17243
- return cmd.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("--level <n>", "Detection level: 1 = regex (default), 2 = AST/data-flow (falls back to 1 unless the native addon is built locally)", "1").option("--ast", "Alias for --level 2 (falls back to level 1 unless the native addon is built locally)").option(
16876
+ return cmd.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(
17244
16877
  "--policy <preset>",
17245
16878
  `Apply a compliance preset (${COMPLIANCE_PRESET_IDS.join(", ")}); overrides scan.compliancePreset in pretense.yaml`
17246
16879
  );
@@ -17334,7 +16967,7 @@ function pathSubcommand() {
17334
16967
  });
17335
16968
  }
17336
16969
  function scanCommand() {
17337
- const cmd = new Command3("scan").description("Scan for secrets and PII (file, directory, or git-aware modes)").argument("[file]", "File to scan (omit to read from stdin)").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("--level <n>", "Detection level: 1 = regex (default), 2 = AST/data-flow (falls back to 1 unless the native addon is built locally)", "1").option("--ast", "Alias for --level 2 (falls back to level 1 unless the native addon is built locally)").option(
16970
+ const cmd = new Command3("scan").description("Scan for secrets and PII (file, directory, or git-aware modes)").argument("[file]", "File to scan (omit to read from stdin)").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(
17338
16971
  "--policy <preset>",
17339
16972
  `Apply a compliance preset (${COMPLIANCE_PRESET_IDS.join(", ")}); overrides scan.compliancePreset in pretense.yaml`
17340
16973
  ).action(async (file, opts = {}) => {
@@ -17379,14 +17012,12 @@ function scanCommand() {
17379
17012
  for await (const chunk of process.stdin) chunks.push(chunk);
17380
17013
  content = Buffer.concat(chunks).toString("utf-8");
17381
17014
  }
17382
- const { level, astUsed } = resolveLevelWithAst(opts);
17383
- const effectiveLevel = effectiveScanLevel(level, astUsed);
17384
17015
  const rawResult = await scanContent(content, opts, file);
17385
17016
  const filteredMatches = filterBySeverity(rawResult.matches, opts?.severity);
17386
17017
  const result = { ...rawResult, matches: filteredMatches, clean: filteredMatches.length === 0 };
17387
17018
  const fmt2 = resolveFormat(opts ?? {});
17388
17019
  if (fmt2 === "json") {
17389
- console.log(JSON.stringify({ ...result, level: effectiveLevel, requestedLevel: level }, null, 2));
17020
+ console.log(JSON.stringify(result, null, 2));
17390
17021
  process.exit(result.clean ? 0 : 1);
17391
17022
  }
17392
17023
  if (fmt2 === "sarif") {
@@ -17402,7 +17033,7 @@ function scanCommand() {
17402
17033
  console.log(chalk3.cyan(`
17403
17034
  Pretense Scan \u2014 ${file ?? "stdin"}
17404
17035
  `));
17405
- console.log(chalk3.dim(` Patterns: ${getPatternCount()} | Level: ${levelLabel(level, astUsed)} | Duration: ${result.durationMs}ms
17036
+ console.log(chalk3.dim(` Patterns: ${getPatternCount()} | Duration: ${result.durationMs}ms
17406
17037
  `));
17407
17038
  if (result.clean) {
17408
17039
  console.log(chalk3.green(" \u2713 No secrets or PII detected\n"));
@@ -18836,7 +18467,7 @@ import { Command as Command14 } from "commander";
18836
18467
  import chalk13 from "chalk";
18837
18468
  import { readFileSync as readFileSync14, writeFileSync as writeFileSync9, chmodSync as chmodSync6 } from "fs";
18838
18469
  import { createHash as createHash3 } from "crypto";
18839
- import { resolve as resolve4, extname as extname4 } from "path";
18470
+ import { resolve as resolve4 } from "path";
18840
18471
 
18841
18472
  // src/secret-crypto.ts
18842
18473
  init_esm_shims();
@@ -18932,81 +18563,16 @@ var REVERSAL_MAP_VERSION = 2;
18932
18563
  function sha256Hex2(s) {
18933
18564
  return createHash3("sha256").update(s, "utf8").digest("hex");
18934
18565
  }
18935
- function langForFile2(filePath) {
18936
- switch (extname4(filePath).toLowerCase()) {
18937
- case ".py":
18938
- return "python";
18939
- case ".go":
18940
- return "go";
18941
- case ".java":
18942
- return "java";
18943
- case ".rb":
18944
- return "ruby";
18945
- case ".rs":
18946
- return "rust";
18947
- case ".cs":
18948
- return "csharp";
18949
- case ".js":
18950
- case ".jsx":
18951
- return "javascript";
18952
- case ".tsx":
18953
- case ".ts":
18954
- return "typescript";
18955
- default:
18956
- return "typescript";
18957
- }
18958
- }
18959
- function resolveMutateLevel(opts) {
18960
- if (opts.ast) return 2;
18961
- const raw2 = opts.level;
18962
- if (raw2 === void 0 || raw2 === "1") return 1;
18963
- if (raw2 === "2") return 2;
18964
- console.error(chalk13.red(`
18965
- x Invalid --level: "${raw2}". Use 1 (regex) or 2 (AST/data-flow).
18966
- `));
18967
- process.exit(1);
18968
- }
18969
- function overlaps2(aOff, aLen, bOff, bLen) {
18970
- return aOff < bOff + bLen && bOff < aOff + aLen;
18971
- }
18972
- function innerValueFinding(content, m) {
18973
- if (m.end <= m.start || m.value.length === 0) return null;
18974
- const raw2 = content.slice(m.start, m.end);
18975
- const idx = raw2.indexOf(m.value);
18976
- if (idx < 0) return null;
18977
- return { value: m.value, offset: m.start + idx, length: m.value.length, type: m.type };
18978
- }
18979
- async function collectMutationFindings(content, file, level) {
18566
+ async function collectMutationFindings(content, file) {
18980
18567
  const l1 = scan(content, { contextAware: true, entropyAnalysis: true, filePath: file });
18981
- const findings = l1.matches.map((m) => ({
18568
+ return l1.matches.map((m) => ({
18982
18569
  value: m.value,
18983
18570
  offset: m.start,
18984
18571
  length: m.end - m.start,
18985
18572
  type: m.type
18986
18573
  }));
18987
- if (level === 2) {
18988
- if (!isAstCoreAvailable()) {
18989
- console.error(
18990
- chalk13.yellow(" ! AST core (@pretense/scanner-rs) unavailable \u2014 mutating with L1 (regex).")
18991
- );
18992
- }
18993
- const l2 = await scanLevel2(content, langForFile2(file), {
18994
- contextAware: true,
18995
- entropyAnalysis: true,
18996
- filePath: file
18997
- });
18998
- for (const m of l2.matches) {
18999
- if (m.type !== "dataflow-credential") continue;
19000
- const f = innerValueFinding(content, m);
19001
- if (!f) continue;
19002
- if (findings.some((e) => overlaps2(f.offset, f.length, e.offset, e.length))) continue;
19003
- findings.push(f);
19004
- }
19005
- }
19006
- return findings;
19007
18574
  }
19008
18575
  async function runMutate(file, opts = {}) {
19009
- const level = resolveMutateLevel(opts);
19010
18576
  let raw2;
19011
18577
  try {
19012
18578
  raw2 = readFileSync14(file);
@@ -19027,7 +18593,7 @@ async function runMutate(file, opts = {}) {
19027
18593
  );
19028
18594
  return 1;
19029
18595
  }
19030
- const findings = await collectMutationFindings(content, file, level);
18596
+ const findings = await collectMutationFindings(content, file);
19031
18597
  if (findings.length === 0) {
19032
18598
  console.error(chalk13.dim(` No secrets found in ${file} \u2014 nothing to mutate.`));
19033
18599
  if (!opts.inPlace && !opts.output) process.stdout.write(content);
@@ -19093,7 +18659,7 @@ async function runMutate(file, opts = {}) {
19093
18659
  return 0;
19094
18660
  }
19095
18661
  function mutateCommand() {
19096
- return new Command14("mutate").description("Mutate secrets in a file to deterministic synthetics").argument("<file>", "File to mutate").option("-i, --in-place", "Write back to the input file").option("-o, --output <path>", "Write mutated content to a specific file").option("--map <path>", "Write the encrypted reversal map (for `pretense reverse`) to a file").option("--level <n>", "Detection level: 1 = regex (default), 2 = AST/data-flow", "1").option("--ast", "Alias for --level 2 (AST/data-flow analysis)").option("--show-map", "Print mutation map JSON to stderr").action(async (file, opts) => {
18662
+ return new Command14("mutate").description("Mutate secrets in a file to deterministic synthetics").argument("<file>", "File to mutate").option("-i, --in-place", "Write back to the input file").option("-o, --output <path>", "Write mutated content to a specific file").option("--map <path>", "Write the encrypted reversal map (for `pretense reverse`) to a file").option("--show-map", "Print mutation map JSON to stderr").action(async (file, opts) => {
19097
18663
  const code = await runMutate(file, opts);
19098
18664
  process.exit(code);
19099
18665
  });
@@ -19604,7 +19170,7 @@ function findClosestCommand(input, knownCommands) {
19604
19170
  return bestDist <= 3 ? best : null;
19605
19171
  }
19606
19172
  var program = new Command20();
19607
- program.name("pretense").description(chalk19.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version("0.6.4", "-v, --version").addHelpText("after", `
19173
+ program.name("pretense").description(chalk19.bold("AI firewall") + " \u2014 mutates proprietary code before LLM API calls").version("0.6.5", "-v, --version").addHelpText("after", `
19608
19174
  ${chalk19.bold("Quick start:")}
19609
19175
  ${chalk19.cyan("pretense scan")} ${chalk19.dim("<file|dir>")} Scan for secrets and PII
19610
19176
  ${chalk19.cyan("pretense mutate")} ${chalk19.dim("<file>")} Preview code mutations
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pretense/cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Local-first AI firewall that mutates proprietary code before sending to LLM APIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,14 +11,6 @@
11
11
  "README.md",
12
12
  "LICENSE"
13
13
  ],
14
- "scripts": {
15
- "build": "tsup",
16
- "dev": "tsx src/index.ts",
17
- "prepublishOnly": "pnpm build",
18
- "postinstall": "node -e \"fs.existsSync('dist/postinstall.js') && import('./dist/postinstall.js')\" || true",
19
- "test": "vitest run",
20
- "lint": "tsc --noEmit"
21
- },
22
14
  "dependencies": {
23
15
  "better-sqlite3": "^11.0.0",
24
16
  "chalk": "^5.3.0",
@@ -26,19 +18,19 @@
26
18
  "yaml": "^2.4.0"
27
19
  },
28
20
  "devDependencies": {
29
- "@pretense/billing": "workspace:*",
30
- "@pretense/compliance-engine": "workspace:*",
31
- "@pretense/protocol": "workspace:*",
32
- "@pretense/learner": "workspace:*",
33
- "@pretense/mutator": "workspace:*",
34
- "@pretense/proxy": "workspace:*",
35
- "@pretense/scanner": "workspace:*",
36
- "@pretense/store": "workspace:*",
37
21
  "@types/node": "^20.0.0",
38
22
  "tsup": "^8.0.0",
39
23
  "tsx": "^4.0.0",
40
24
  "typescript": "^5.4.0",
41
- "vitest": "^1.0.0"
25
+ "vitest": "^1.0.0",
26
+ "@pretense/protocol": "0.1.0",
27
+ "@pretense/learner": "0.2.0",
28
+ "@pretense/mutator": "0.2.0",
29
+ "@pretense/scanner": "0.2.0",
30
+ "@pretense/store": "0.2.0",
31
+ "@pretense/proxy": "0.1.0",
32
+ "@pretense/compliance-engine": "0.1.0",
33
+ "@pretense/billing": "0.1.0"
42
34
  },
43
35
  "publishConfig": {
44
36
  "access": "public",
@@ -78,5 +70,12 @@
78
70
  },
79
71
  "engines": {
80
72
  "node": ">=20.0.0"
73
+ },
74
+ "scripts": {
75
+ "build": "tsup",
76
+ "dev": "tsx src/index.ts",
77
+ "postinstall": "node -e \"fs.existsSync('dist/postinstall.js') && import('./dist/postinstall.js')\" || true",
78
+ "test": "vitest run",
79
+ "lint": "tsc --noEmit"
81
80
  }
82
- }
81
+ }