@methiu/raigal 0.2.6 → 0.2.7

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as APP_VERSION } from "./version-DtOeK-2T.js";
1
+ import { t as APP_VERSION } from "./version-CXMYu17z.js";
2
2
  import { a as getEngineLabel, i as ENGINE_INFO, r as scoreImpactForRule, t as summarizeFindingAssessments } from "./finding-assessment-CGpt5YUR.js";
3
3
  import { n as isSecretClassRule, r as maskSecrets, t as FIXED_SECRET_DIAGNOSTIC_MESSAGE } from "./mask-secrets-BXn-ZSEh.js";
4
4
  import { createRequire, isBuiltin } from "node:module";
@@ -1357,7 +1357,28 @@ const isSafeRegularProjectFile = (rootDirectory, absolutePath) => {
1357
1357
  return false;
1358
1358
  }
1359
1359
  };
1360
- const hasAllowedSourceExtension = (filePath, extraExtensions) => SOURCE_EXTENSIONS$1.has(path.extname(filePath)) || extraExtensions.has(path.extname(filePath));
1360
+ const SECRET_CONFIG_EXTENSIONS = [
1361
+ ".env",
1362
+ ".yaml",
1363
+ ".yml",
1364
+ ".json",
1365
+ ".toml",
1366
+ ".ini",
1367
+ ".properties",
1368
+ ".tfvars"
1369
+ ];
1370
+ const LOCKFILE_RE = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|packages\.lock\.json|composer\.lock|bun\.lock)$/i;
1371
+ const isDotenvFile = (filePath) => {
1372
+ const base = path.basename(filePath).toLowerCase();
1373
+ return base === ".env" || base.startsWith(".env.");
1374
+ };
1375
+ const hasAllowedSourceExtension = (filePath, extraExtensions) => {
1376
+ const extension = path.extname(filePath);
1377
+ if (SOURCE_EXTENSIONS$1.has(extension)) return true;
1378
+ if (LOCKFILE_RE.test(filePath)) return false;
1379
+ if (extraExtensions.has(".env") && isDotenvFile(filePath)) return true;
1380
+ return extraExtensions.has(extension);
1381
+ };
1361
1382
  const isInExcludedDirectory = (filePath, excludedDirectories) => {
1362
1383
  const normalized = filePath.toLowerCase();
1363
1384
  return excludedDirectories.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`) || normalized.includes(`/${directory}/`));
@@ -2588,6 +2609,7 @@ const collectScanFileScope = (request) => {
2588
2609
  projectFiles: [.../* @__PURE__ */ new Set([...projectSourceFiles, ...declarations])],
2589
2610
  scoreFileCount: projectSourceFiles.length + projectTestFiles.length,
2590
2611
  scopeLabel: candidates.label,
2612
+ secretFiles: filterEnumeratedProjectFiles(request.rootDirectory, candidates.files, [...SECRET_CONFIG_EXTENSIONS], request.excludePatterns, request.includePatterns),
2591
2613
  testFiles
2592
2614
  };
2593
2615
  };
@@ -14807,13 +14829,19 @@ const SECRET_PATTERNS = [
14807
14829
  },
14808
14830
  {
14809
14831
  pattern: /AKIA[0-9A-Z]{16}/g,
14810
- name: "AWS Access Key"
14832
+ name: "AWS Access Key",
14833
+ specific: true
14811
14834
  },
14812
14835
  {
14813
- pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
14836
+ pattern: /(?:aws[_-]?secret(?:[_-]?access)?[_-]?key|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
14814
14837
  name: "AWS Secret Key",
14815
14838
  keywordPrefixed: true
14816
14839
  },
14840
+ {
14841
+ pattern: /\b(?:sk|rk)_live_[A-Za-z0-9]{20,}/g,
14842
+ name: "Stripe secret key",
14843
+ specific: true
14844
+ },
14817
14845
  {
14818
14846
  pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
14819
14847
  name: "Hardcoded password/secret",
@@ -14821,11 +14849,13 @@ const SECRET_PATTERNS = [
14821
14849
  },
14822
14850
  {
14823
14851
  pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
14824
- name: "Private key"
14852
+ name: "Private key",
14853
+ specific: true
14825
14854
  },
14826
14855
  {
14827
14856
  pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
14828
- name: "JWT token"
14857
+ name: "JWT token",
14858
+ specific: true
14829
14859
  },
14830
14860
  {
14831
14861
  pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
@@ -14834,17 +14864,51 @@ const SECRET_PATTERNS = [
14834
14864
  },
14835
14865
  {
14836
14866
  pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
14837
- name: "GitHub token"
14867
+ name: "GitHub token",
14868
+ specific: true
14838
14869
  },
14839
14870
  {
14840
14871
  pattern: /xox[baprs]-[A-Za-z0-9-]+/g,
14841
- name: "Slack token"
14872
+ name: "Slack token",
14873
+ specific: true
14842
14874
  },
14843
14875
  {
14844
14876
  pattern: /(?:mongodb(?:\+srv)?|postgres|mysql|redis):\/\/[^:@/"'`\s]+:[^@"'`\s]+@[^"'`\s]+/gi,
14845
- name: "Database connection string with credentials"
14877
+ name: "Database connection string with credentials",
14878
+ specific: true
14846
14879
  }
14847
14880
  ];
14881
+ const FORMAT_PATTERNS = SECRET_PATTERNS.filter((p) => p.specific);
14882
+ const CONFIG_ASSIGNMENT_RE = /([A-Za-z0-9_.-]*(?:password|passwd|pwd|secret|token|api[_-]?key|apikey)[A-Za-z0-9_.-]*)["']?[ \t]*[:=][ \t]*["']?([^\s"'#,]+)/gi;
14883
+ const CONFIG_REFERENCE_KEY_RE = /(?:name|ref|path|file|arn|id|type|version|url|uri|length|count|env|header|field|prefix|policy|manager|store|provider)$/i;
14884
+ const CONFIG_REFERENCE_VALUE_RE = /^[<{$%]/;
14885
+ const CONFIG_PLACEHOLDER_VALUE_RE = /^(?:your|example|placeholder|redacted|dummy|xxx)/i;
14886
+ const VERSION_VALUE_RE = /^[~^<>=v]*\d+(?:\.\d+)+/;
14887
+ const CONFIG_LITERAL_VALUES = /* @__PURE__ */ new Set([
14888
+ "true",
14889
+ "false",
14890
+ "null",
14891
+ "none",
14892
+ "nil"
14893
+ ]);
14894
+ const isConfigFile = (filePath) => isDotenvFile(filePath) || SECRET_CONFIG_EXTENSIONS.includes(path.extname(filePath).toLowerCase());
14895
+ const DOTENV_TEMPLATE_RE = /^\.env\.(?:example|sample|template|dist|defaults)$/i;
14896
+ const isLocalDatabaseUrl = (value) => {
14897
+ try {
14898
+ return LOCAL_DB_HOSTS.has(new URL(value).hostname.toLowerCase());
14899
+ } catch {
14900
+ return false;
14901
+ }
14902
+ };
14903
+ const configAssignmentName = (key) => {
14904
+ const lower = key.toLowerCase();
14905
+ if (/aws[_-]?secret/.test(lower)) return "AWS Secret Key";
14906
+ if (/api[_-]?key|apikey/.test(lower)) return "API key";
14907
+ if (/token/.test(lower) && !/password|passwd|pwd|secret/.test(lower)) return "Authentication token";
14908
+ return "Hardcoded password/secret";
14909
+ };
14910
+ const URL_VALUE_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
14911
+ const isConfigValueIgnorable = (key, value) => value.length < 8 || URL_VALUE_RE.test(value) || CONFIG_REFERENCE_KEY_RE.test(key) || CONFIG_REFERENCE_VALUE_RE.test(value) || CONFIG_PLACEHOLDER_VALUE_RE.test(value) || VERSION_VALUE_RE.test(value) || CONFIG_LITERAL_VALUES.has(value.toLowerCase()) || isNonProductionCredentialValue(value);
14848
14912
  const isInsideStringLiteral = (content, matchIndex) => {
14849
14913
  const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
14850
14914
  const prefix = content.slice(lineStart, matchIndex);
@@ -14989,16 +15053,69 @@ const shouldSkipSecretFinding = (relativePath, name, lineText, matchedText) => {
14989
15053
  if ((name === "Hardcoded password/secret" || name === "Authentication token") && isSymbolicConstantValue(matchedText)) return true;
14990
15054
  return false;
14991
15055
  };
15056
+ const collectSourceCandidates = (content, relativePath) => {
15057
+ const candidates = [];
15058
+ for (const { pattern, name, keywordPrefixed, specific } of SECRET_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
15059
+ const matchedText = match[1] ?? match[0];
15060
+ const lineText = lineForMatch(content, match.index);
15061
+ if (isPlaceholderValue(matchedText)) continue;
15062
+ if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
15063
+ if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
15064
+ candidates.push({
15065
+ name,
15066
+ start: match.index,
15067
+ end: match.index + match[0].length,
15068
+ specific: Boolean(specific)
15069
+ });
15070
+ }
15071
+ return candidates;
15072
+ };
15073
+ const collectConfigCandidates = (content, relativePath) => {
15074
+ const candidates = [];
15075
+ const template = DOTENV_TEMPLATE_RE.test(path.posix.basename(relativePath));
15076
+ for (const { pattern, name } of FORMAT_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
15077
+ const lineText = lineForMatch(content, match.index);
15078
+ if (isPlaceholderValue(match[0])) continue;
15079
+ if (template && isLocalDatabaseUrl(match[0])) continue;
15080
+ if (shouldSkipSecretFinding(relativePath, name, lineText, match[0])) continue;
15081
+ candidates.push({
15082
+ name,
15083
+ start: match.index,
15084
+ end: match.index + match[0].length,
15085
+ specific: true
15086
+ });
15087
+ }
15088
+ for (const match of content.matchAll(new RegExp(CONFIG_ASSIGNMENT_RE.source, CONFIG_ASSIGNMENT_RE.flags))) {
15089
+ const [, key, value] = match;
15090
+ const name = configAssignmentName(key);
15091
+ const lineText = lineForMatch(content, match.index);
15092
+ if (isConfigValueIgnorable(key, value) || isPlaceholderValue(value)) continue;
15093
+ if (shouldSkipSecretFinding(relativePath, name, lineText, value)) continue;
15094
+ candidates.push({
15095
+ name,
15096
+ start: match.index,
15097
+ end: match.index + match[0].length,
15098
+ specific: false
15099
+ });
15100
+ }
15101
+ return candidates;
15102
+ };
15103
+ const dedupeCandidates = (candidates) => {
15104
+ const ordered = [...candidates].sort((a, b) => Number(b.specific) - Number(a.specific) || a.start - b.start);
15105
+ const kept = [];
15106
+ for (const candidate of ordered) {
15107
+ if (kept.some((k) => candidate.start < k.end && k.start < candidate.end)) continue;
15108
+ kept.push(candidate);
15109
+ }
15110
+ return kept.sort((a, b) => a.start - b.start);
15111
+ };
15112
+ const resolveSecretFiles = (context) => {
15113
+ const extras = [...SECRET_CONFIG_EXTENSIONS];
15114
+ return context.secretFiles ? filterExplicitFiles(context.rootDirectory, context.secretFiles, extras) : getSourceFilesWithExtras(context, extras);
15115
+ };
14992
15116
  const scanSecrets = async (context) => {
14993
- const files = getSourceFilesWithExtras(context, [
14994
- ".env",
14995
- ".yaml",
14996
- ".yml",
14997
- ".json",
14998
- ".toml"
14999
- ]);
15000
15117
  const diagnostics = [];
15001
- for (const filePath of files) {
15118
+ for (const filePath of resolveSecretFiles(context)) {
15002
15119
  let content;
15003
15120
  try {
15004
15121
  content = fs.readFileSync(filePath, "utf-8");
@@ -15007,30 +15124,20 @@ const scanSecrets = async (context) => {
15007
15124
  }
15008
15125
  content = maskComments(content, path.extname(filePath));
15009
15126
  const relativePath = relativePosix(context.rootDirectory, filePath);
15010
- for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
15011
- const regex = new RegExp(pattern.source, pattern.flags);
15012
- for (const match of content.matchAll(regex)) {
15013
- const matchedText = match[1] ?? match[0];
15014
- const lineText = lineForMatch(content, match.index);
15015
- if (isPlaceholderValue(matchedText)) continue;
15016
- if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
15017
- if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
15018
- const line = content.slice(0, match.index).split("\n").length;
15019
- diagnostics.push({
15020
- filePath: relativePath,
15021
- engine: "security",
15022
- rule: "security/hardcoded-secret",
15023
- severity: "error",
15024
- message: `Possible ${name} detected in source code`,
15025
- help: "Move secrets to environment variables or a secrets manager",
15026
- line,
15027
- column: 0,
15028
- category: "Security",
15029
- fixable: false,
15030
- redactSource: true
15031
- });
15032
- }
15033
- }
15127
+ const candidates = isConfigFile(filePath) ? collectConfigCandidates(content, relativePath) : collectSourceCandidates(content, relativePath);
15128
+ for (const { name, start } of dedupeCandidates(candidates)) diagnostics.push({
15129
+ filePath: relativePath,
15130
+ engine: "security",
15131
+ rule: "security/hardcoded-secret",
15132
+ severity: "error",
15133
+ message: `Possible ${name} detected in source code`,
15134
+ help: "Move secrets to environment variables or a secrets manager",
15135
+ line: content.slice(0, start).split("\n").length,
15136
+ column: 0,
15137
+ category: "Security",
15138
+ fixable: false,
15139
+ redactSource: true
15140
+ });
15034
15141
  }
15035
15142
  return diagnostics;
15036
15143
  };
@@ -16311,7 +16418,7 @@ const AllowedOwner = z.object({
16311
16418
  login: z.string().max(100)
16312
16419
  });
16313
16420
  const EntitlementClaims = z.object({
16314
- iss: z.literal(process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev"),
16421
+ iss: z.literal("https://app.raigal.dev"),
16315
16422
  sub: z.string(),
16316
16423
  org_name: z.string(),
16317
16424
  plan: z.enum(["trial", "active"]),
@@ -16419,14 +16526,6 @@ const fetchEntitlementFromApi = async (token, oidcToken, orgId) => {
16419
16526
  const EMBEDDED_PUBLIC_KEYS = { rgl_key_2026_1: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA69B+1w8c4yJpEURW4Ps2b4BZcoIzeBg3jkCfIo2n2eM=\n-----END PUBLIC KEY-----" };
16420
16527
  const keyObjectCache = /* @__PURE__ */ new Map();
16421
16528
  const getPublicKey = (kid) => {
16422
- if (process.env.RAIGAL_KEY_ID === kid && process.env.RAIGAL_PUBLIC_KEY) {
16423
- const customKey = process.env.RAIGAL_PUBLIC_KEY.replace(/\\n/g, "\n");
16424
- try {
16425
- return createPublicKey(customKey);
16426
- } catch {
16427
- return null;
16428
- }
16429
- }
16430
16529
  const cached = keyObjectCache.get(kid);
16431
16530
  if (cached) return cached;
16432
16531
  const pem = EMBEDDED_PUBLIC_KEYS[kid];
@@ -16486,9 +16585,8 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
16486
16585
  const parsed = EntitlementClaims.safeParse(rawPayload);
16487
16586
  if (!parsed.success) throw new JwtVerificationError(`Invalid entitlement claims: ${parsed.error.message}`, "invalid_claims");
16488
16587
  const claims = parsed.data;
16489
- const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
16490
- if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
16491
- if (!isVersionAtLeast("0.2.6", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
16588
+ if (claims.iss !== "https://app.raigal.dev") throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
16589
+ if (!isVersionAtLeast("0.2.7", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
16492
16590
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
16493
16591
  const grace = options.allowGraceSeconds ?? 0;
16494
16592
  if (now > claims.exp + grace) {
@@ -19829,22 +19927,10 @@ const printEngineStatus = (result) => {
19829
19927
  }
19830
19928
  };
19831
19929
  //#endregion
19832
- //#region src/commands/scan-hook-nudge.ts
19833
- const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
19834
- const buildHookNudge = (ctx) => {
19835
- if (!shouldShowHookNudge(ctx)) return null;
19836
- return [
19837
- "",
19838
- "Next: install the per-edit hook so your coding agent fixes slop before it lands.",
19839
- ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
19840
- ""
19841
- ].join("\n");
19842
- };
19843
- //#endregion
19844
19930
  //#region src/commands/scan-coverage.ts
19845
19931
  const coverageReason = (c) => {
19846
19932
  if (c.supportedFiles === 0 && c.dominantUnsupported) return `This repository is ${c.dominantUnsupported} (${c.unsupportedFiles} files), which Raigal does not analyze. No score — it would not reflect this code.`;
19847
- if (c.supportedFiles === 0) return "No files in a language Raigal analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Java). Nothing to score.";
19933
+ if (c.supportedFiles === 0) return "No files in a language Raigal analyzes (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, C#, C/C++). Nothing to score.";
19848
19934
  const lang = c.dominantUnsupported ?? "an unsupported language";
19849
19935
  const files = `${c.supportedFiles} supported file${c.supportedFiles === 1 ? "" : "s"}`;
19850
19936
  return `This repository is mostly ${lang} (${c.unsupportedFiles} files); Raigal analyzed only ${files}. Score withheld — it would represent a sliver of the codebase.`;
@@ -20045,6 +20131,18 @@ const computeScanExitCode = (opts) => {
20045
20131
  */
20046
20132
  const computeCiExitCode = (opts) => opts.hasErrors || opts.scoreable && opts.score < opts.failBelow ? 1 : 0;
20047
20133
  //#endregion
20134
+ //#region src/commands/scan-hook-nudge.ts
20135
+ const shouldShowHookNudge = (ctx) => ctx.isTty && !ctx.isCi && ctx.installedAgentCount === 0;
20136
+ const buildHookNudge = (ctx) => {
20137
+ if (!shouldShowHookNudge(ctx)) return null;
20138
+ return [
20139
+ "",
20140
+ "Next: install the per-edit hook so your coding agent fixes slop before it lands.",
20141
+ ctx.invocation === "raigal" ? " raigal hook install" : " npm install -g @methiu/raigal && raigal hook install",
20142
+ ""
20143
+ ].join("\n");
20144
+ };
20145
+ //#endregion
20048
20146
  //#region src/output/rule-labels.ts
20049
20147
  const RULE_LABELS = {
20050
20148
  formatting: "Code not formatted",
@@ -20556,7 +20654,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
20556
20654
  const projectName = projectInfo.projectName ?? "project";
20557
20655
  const language = projectInfo.languages[0] ?? "unknown";
20558
20656
  const printedHumanHeader = !machineOutput && showHeader;
20559
- const { dependencyAuditFiles, dependencyAuditScope, files, projectFiles, scoreFileCount, scopeLabel, testFiles } = scanScope;
20657
+ const { dependencyAuditFiles, dependencyAuditScope, files, projectFiles, scoreFileCount, scopeLabel, secretFiles, testFiles } = scanScope;
20560
20658
  const excludePatterns = [...config.exclude, ...readAislopIgnorePatterns(resolvedDir)];
20561
20659
  const scanCoverage = deriveScanCoverage(projectInfo.coverage, scoreFileCount);
20562
20660
  const reportProjectInfo = {
@@ -20597,6 +20695,7 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
20597
20695
  excludePatterns,
20598
20696
  testFiles,
20599
20697
  projectFiles,
20698
+ secretFiles,
20600
20699
  installedTools: projectInfo.installedTools,
20601
20700
  config: engineConfig
20602
20701
  }, config.engines, machineOutput)).map((result) => ({
@@ -20663,12 +20762,12 @@ const runScanBody = async (resolvedDir, config, options, projectInfo, scanScope,
20663
20762
  scoreFileCount
20664
20763
  });
20665
20764
  if (options.sarif) {
20666
- const { buildSarifLog } = await import("./sarif-Cu7Rm1Ma.js");
20765
+ const { buildSarifLog } = await import("./sarif-CpMse0iL.js");
20667
20766
  console.log(JSON.stringify(buildSarifLog(results), null, 2));
20668
20767
  return completion;
20669
20768
  }
20670
20769
  if (options.json) {
20671
- const { buildJsonOutput } = await import("./json-DxIA7CCt.js");
20770
+ const { buildJsonOutput } = await import("./json-B2Kc7_rw.js");
20672
20771
  const jsonOut = buildJsonOutput(results, scoreResult, scoreFileCount, elapsedMs, scanCoverage);
20673
20772
  console.log(JSON.stringify(jsonOut, null, 2));
20674
20773
  return completion;
@@ -1,4 +1,4 @@
1
- import { t as APP_VERSION } from "./version-DtOeK-2T.js";
1
+ import { t as APP_VERSION } from "./version-CXMYu17z.js";
2
2
  import { i as ENGINE_INFO, n as withFindingAssessments, t as summarizeFindingAssessments } from "./finding-assessment-CGpt5YUR.js";
3
3
  //#region src/output/json.ts
4
4
  const buildJsonOutput = (results, scoreResult, fileCount, elapsedMs, coverage) => {
package/dist/mcp.js CHANGED
@@ -84,7 +84,7 @@ const getActiveToken = () => {
84
84
  };
85
85
  //#endregion
86
86
  //#region src/version.ts
87
- const APP_VERSION = "0.2.6";
87
+ const APP_VERSION = "0.2.7";
88
88
  //#endregion
89
89
  //#region src/cloud/contract.ts
90
90
  const Severity = z.enum(["error", "warning"]);
@@ -185,7 +185,7 @@ const AllowedOwner = z.object({
185
185
  login: z.string().max(100)
186
186
  });
187
187
  const EntitlementClaims = z.object({
188
- iss: z.literal(process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev"),
188
+ iss: z.literal("https://app.raigal.dev"),
189
189
  sub: z.string(),
190
190
  org_name: z.string(),
191
191
  plan: z.enum(["trial", "active"]),
@@ -293,14 +293,6 @@ const fetchEntitlementFromApi = async (token, oidcToken, orgId) => {
293
293
  const EMBEDDED_PUBLIC_KEYS = { rgl_key_2026_1: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA69B+1w8c4yJpEURW4Ps2b4BZcoIzeBg3jkCfIo2n2eM=\n-----END PUBLIC KEY-----" };
294
294
  const keyObjectCache = /* @__PURE__ */ new Map();
295
295
  const getPublicKey = (kid) => {
296
- if (process.env.RAIGAL_KEY_ID === kid && process.env.RAIGAL_PUBLIC_KEY) {
297
- const customKey = process.env.RAIGAL_PUBLIC_KEY.replace(/\\n/g, "\n");
298
- try {
299
- return createPublicKey(customKey);
300
- } catch {
301
- return null;
302
- }
303
- }
304
296
  const cached = keyObjectCache.get(kid);
305
297
  if (cached) return cached;
306
298
  const pem = EMBEDDED_PUBLIC_KEYS[kid];
@@ -360,9 +352,8 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
360
352
  const parsed = EntitlementClaims.safeParse(rawPayload);
361
353
  if (!parsed.success) throw new JwtVerificationError(`Invalid entitlement claims: ${parsed.error.message}`, "invalid_claims");
362
354
  const claims = parsed.data;
363
- const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
364
- if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
365
- if (!isVersionAtLeast("0.2.6", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
355
+ if (claims.iss !== "https://app.raigal.dev") throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
356
+ if (!isVersionAtLeast("0.2.7", claims.min_cli_version)) throw new JwtVerificationError(`CLI version ${APP_VERSION} is below required minimum ${claims.min_cli_version}. Run "raigal update" or "npm install -g @methiu/raigal@latest".`, "cli_version_unsupported");
366
357
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
367
358
  const grace = options.allowGraceSeconds ?? 0;
368
359
  if (now > claims.exp + grace) {
@@ -1872,7 +1863,28 @@ const isSafeRegularProjectFile = (rootDirectory, absolutePath) => {
1872
1863
  return false;
1873
1864
  }
1874
1865
  };
1875
- const hasAllowedSourceExtension = (filePath, extraExtensions) => SOURCE_EXTENSIONS$1.has(path.extname(filePath)) || extraExtensions.has(path.extname(filePath));
1866
+ const SECRET_CONFIG_EXTENSIONS = [
1867
+ ".env",
1868
+ ".yaml",
1869
+ ".yml",
1870
+ ".json",
1871
+ ".toml",
1872
+ ".ini",
1873
+ ".properties",
1874
+ ".tfvars"
1875
+ ];
1876
+ const LOCKFILE_RE = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|pnpm-lock\.yaml|packages\.lock\.json|composer\.lock|bun\.lock)$/i;
1877
+ const isDotenvFile = (filePath) => {
1878
+ const base = path.basename(filePath).toLowerCase();
1879
+ return base === ".env" || base.startsWith(".env.");
1880
+ };
1881
+ const hasAllowedSourceExtension = (filePath, extraExtensions) => {
1882
+ const extension = path.extname(filePath);
1883
+ if (SOURCE_EXTENSIONS$1.has(extension)) return true;
1884
+ if (LOCKFILE_RE.test(filePath)) return false;
1885
+ if (extraExtensions.has(".env") && isDotenvFile(filePath)) return true;
1886
+ return extraExtensions.has(extension);
1887
+ };
1876
1888
  const isInExcludedDirectory = (filePath, excludedDirectories) => {
1877
1889
  const normalized = filePath.toLowerCase();
1878
1890
  return excludedDirectories.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`) || normalized.includes(`/${directory}/`));
@@ -13796,13 +13808,19 @@ const SECRET_PATTERNS = [
13796
13808
  },
13797
13809
  {
13798
13810
  pattern: /AKIA[0-9A-Z]{16}/g,
13799
- name: "AWS Access Key"
13811
+ name: "AWS Access Key",
13812
+ specific: true
13800
13813
  },
13801
13814
  {
13802
- pattern: /(?:aws[_-]?secret|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
13815
+ pattern: /(?:aws[_-]?secret(?:[_-]?access)?[_-]?key|secret[_-]?key)\s*[:=]\s*["']([A-Za-z0-9/+=]{40})["']/gi,
13803
13816
  name: "AWS Secret Key",
13804
13817
  keywordPrefixed: true
13805
13818
  },
13819
+ {
13820
+ pattern: /\b(?:sk|rk)_live_[A-Za-z0-9]{20,}/g,
13821
+ name: "Stripe secret key",
13822
+ specific: true
13823
+ },
13806
13824
  {
13807
13825
  pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
13808
13826
  name: "Hardcoded password/secret",
@@ -13810,11 +13828,13 @@ const SECRET_PATTERNS = [
13810
13828
  },
13811
13829
  {
13812
13830
  pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
13813
- name: "Private key"
13831
+ name: "Private key",
13832
+ specific: true
13814
13833
  },
13815
13834
  {
13816
13835
  pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
13817
- name: "JWT token"
13836
+ name: "JWT token",
13837
+ specific: true
13818
13838
  },
13819
13839
  {
13820
13840
  pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
@@ -13823,17 +13843,51 @@ const SECRET_PATTERNS = [
13823
13843
  },
13824
13844
  {
13825
13845
  pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
13826
- name: "GitHub token"
13846
+ name: "GitHub token",
13847
+ specific: true
13827
13848
  },
13828
13849
  {
13829
13850
  pattern: /xox[baprs]-[A-Za-z0-9-]+/g,
13830
- name: "Slack token"
13851
+ name: "Slack token",
13852
+ specific: true
13831
13853
  },
13832
13854
  {
13833
13855
  pattern: /(?:mongodb(?:\+srv)?|postgres|mysql|redis):\/\/[^:@/"'`\s]+:[^@"'`\s]+@[^"'`\s]+/gi,
13834
- name: "Database connection string with credentials"
13856
+ name: "Database connection string with credentials",
13857
+ specific: true
13835
13858
  }
13836
13859
  ];
13860
+ const FORMAT_PATTERNS = SECRET_PATTERNS.filter((p) => p.specific);
13861
+ const CONFIG_ASSIGNMENT_RE = /([A-Za-z0-9_.-]*(?:password|passwd|pwd|secret|token|api[_-]?key|apikey)[A-Za-z0-9_.-]*)["']?[ \t]*[:=][ \t]*["']?([^\s"'#,]+)/gi;
13862
+ const CONFIG_REFERENCE_KEY_RE = /(?:name|ref|path|file|arn|id|type|version|url|uri|length|count|env|header|field|prefix|policy|manager|store|provider)$/i;
13863
+ const CONFIG_REFERENCE_VALUE_RE = /^[<{$%]/;
13864
+ const CONFIG_PLACEHOLDER_VALUE_RE = /^(?:your|example|placeholder|redacted|dummy|xxx)/i;
13865
+ const VERSION_VALUE_RE = /^[~^<>=v]*\d+(?:\.\d+)+/;
13866
+ const CONFIG_LITERAL_VALUES = /* @__PURE__ */ new Set([
13867
+ "true",
13868
+ "false",
13869
+ "null",
13870
+ "none",
13871
+ "nil"
13872
+ ]);
13873
+ const isConfigFile = (filePath) => isDotenvFile(filePath) || SECRET_CONFIG_EXTENSIONS.includes(path.extname(filePath).toLowerCase());
13874
+ const DOTENV_TEMPLATE_RE = /^\.env\.(?:example|sample|template|dist|defaults)$/i;
13875
+ const isLocalDatabaseUrl = (value) => {
13876
+ try {
13877
+ return LOCAL_DB_HOSTS.has(new URL(value).hostname.toLowerCase());
13878
+ } catch {
13879
+ return false;
13880
+ }
13881
+ };
13882
+ const configAssignmentName = (key) => {
13883
+ const lower = key.toLowerCase();
13884
+ if (/aws[_-]?secret/.test(lower)) return "AWS Secret Key";
13885
+ if (/api[_-]?key|apikey/.test(lower)) return "API key";
13886
+ if (/token/.test(lower) && !/password|passwd|pwd|secret/.test(lower)) return "Authentication token";
13887
+ return "Hardcoded password/secret";
13888
+ };
13889
+ const URL_VALUE_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
13890
+ const isConfigValueIgnorable = (key, value) => value.length < 8 || URL_VALUE_RE.test(value) || CONFIG_REFERENCE_KEY_RE.test(key) || CONFIG_REFERENCE_VALUE_RE.test(value) || CONFIG_PLACEHOLDER_VALUE_RE.test(value) || VERSION_VALUE_RE.test(value) || CONFIG_LITERAL_VALUES.has(value.toLowerCase()) || isNonProductionCredentialValue(value);
13837
13891
  const isInsideStringLiteral = (content, matchIndex) => {
13838
13892
  const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
13839
13893
  const prefix = content.slice(lineStart, matchIndex);
@@ -13978,16 +14032,69 @@ const shouldSkipSecretFinding = (relativePath, name, lineText, matchedText) => {
13978
14032
  if ((name === "Hardcoded password/secret" || name === "Authentication token") && isSymbolicConstantValue(matchedText)) return true;
13979
14033
  return false;
13980
14034
  };
14035
+ const collectSourceCandidates = (content, relativePath) => {
14036
+ const candidates = [];
14037
+ for (const { pattern, name, keywordPrefixed, specific } of SECRET_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
14038
+ const matchedText = match[1] ?? match[0];
14039
+ const lineText = lineForMatch(content, match.index);
14040
+ if (isPlaceholderValue(matchedText)) continue;
14041
+ if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
14042
+ if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
14043
+ candidates.push({
14044
+ name,
14045
+ start: match.index,
14046
+ end: match.index + match[0].length,
14047
+ specific: Boolean(specific)
14048
+ });
14049
+ }
14050
+ return candidates;
14051
+ };
14052
+ const collectConfigCandidates = (content, relativePath) => {
14053
+ const candidates = [];
14054
+ const template = DOTENV_TEMPLATE_RE.test(path.posix.basename(relativePath));
14055
+ for (const { pattern, name } of FORMAT_PATTERNS) for (const match of content.matchAll(new RegExp(pattern.source, pattern.flags))) {
14056
+ const lineText = lineForMatch(content, match.index);
14057
+ if (isPlaceholderValue(match[0])) continue;
14058
+ if (template && isLocalDatabaseUrl(match[0])) continue;
14059
+ if (shouldSkipSecretFinding(relativePath, name, lineText, match[0])) continue;
14060
+ candidates.push({
14061
+ name,
14062
+ start: match.index,
14063
+ end: match.index + match[0].length,
14064
+ specific: true
14065
+ });
14066
+ }
14067
+ for (const match of content.matchAll(new RegExp(CONFIG_ASSIGNMENT_RE.source, CONFIG_ASSIGNMENT_RE.flags))) {
14068
+ const [, key, value] = match;
14069
+ const name = configAssignmentName(key);
14070
+ const lineText = lineForMatch(content, match.index);
14071
+ if (isConfigValueIgnorable(key, value) || isPlaceholderValue(value)) continue;
14072
+ if (shouldSkipSecretFinding(relativePath, name, lineText, value)) continue;
14073
+ candidates.push({
14074
+ name,
14075
+ start: match.index,
14076
+ end: match.index + match[0].length,
14077
+ specific: false
14078
+ });
14079
+ }
14080
+ return candidates;
14081
+ };
14082
+ const dedupeCandidates = (candidates) => {
14083
+ const ordered = [...candidates].sort((a, b) => Number(b.specific) - Number(a.specific) || a.start - b.start);
14084
+ const kept = [];
14085
+ for (const candidate of ordered) {
14086
+ if (kept.some((k) => candidate.start < k.end && k.start < candidate.end)) continue;
14087
+ kept.push(candidate);
14088
+ }
14089
+ return kept.sort((a, b) => a.start - b.start);
14090
+ };
14091
+ const resolveSecretFiles = (context) => {
14092
+ const extras = [...SECRET_CONFIG_EXTENSIONS];
14093
+ return context.secretFiles ? filterExplicitFiles(context.rootDirectory, context.secretFiles, extras) : getSourceFilesWithExtras(context, extras);
14094
+ };
13981
14095
  const scanSecrets = async (context) => {
13982
- const files = getSourceFilesWithExtras(context, [
13983
- ".env",
13984
- ".yaml",
13985
- ".yml",
13986
- ".json",
13987
- ".toml"
13988
- ]);
13989
14096
  const diagnostics = [];
13990
- for (const filePath of files) {
14097
+ for (const filePath of resolveSecretFiles(context)) {
13991
14098
  let content;
13992
14099
  try {
13993
14100
  content = fs.readFileSync(filePath, "utf-8");
@@ -13996,30 +14103,20 @@ const scanSecrets = async (context) => {
13996
14103
  }
13997
14104
  content = maskComments(content, path.extname(filePath));
13998
14105
  const relativePath = relativePosix(context.rootDirectory, filePath);
13999
- for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
14000
- const regex = new RegExp(pattern.source, pattern.flags);
14001
- for (const match of content.matchAll(regex)) {
14002
- const matchedText = match[1] ?? match[0];
14003
- const lineText = lineForMatch(content, match.index);
14004
- if (isPlaceholderValue(matchedText)) continue;
14005
- if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
14006
- if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
14007
- const line = content.slice(0, match.index).split("\n").length;
14008
- diagnostics.push({
14009
- filePath: relativePath,
14010
- engine: "security",
14011
- rule: "security/hardcoded-secret",
14012
- severity: "error",
14013
- message: `Possible ${name} detected in source code`,
14014
- help: "Move secrets to environment variables or a secrets manager",
14015
- line,
14016
- column: 0,
14017
- category: "Security",
14018
- fixable: false,
14019
- redactSource: true
14020
- });
14021
- }
14022
- }
14106
+ const candidates = isConfigFile(filePath) ? collectConfigCandidates(content, relativePath) : collectSourceCandidates(content, relativePath);
14107
+ for (const { name, start } of dedupeCandidates(candidates)) diagnostics.push({
14108
+ filePath: relativePath,
14109
+ engine: "security",
14110
+ rule: "security/hardcoded-secret",
14111
+ severity: "error",
14112
+ message: `Possible ${name} detected in source code`,
14113
+ help: "Move secrets to environment variables or a secrets manager",
14114
+ line: content.slice(0, start).split("\n").length,
14115
+ column: 0,
14116
+ category: "Security",
14117
+ fixable: false,
14118
+ redactSource: true
14119
+ });
14023
14120
  }
14024
14121
  return diagnostics;
14025
14122
  };
@@ -1,4 +1,4 @@
1
- import { t as APP_VERSION } from "./version-DtOeK-2T.js";
1
+ import { t as APP_VERSION } from "./version-CXMYu17z.js";
2
2
  import { n as isSecretClassRule, t as FIXED_SECRET_DIAGNOSTIC_MESSAGE } from "./mask-secrets-BXn-ZSEh.js";
3
3
  import path from "node:path";
4
4
  //#region src/output/sarif.ts