@methiu/raigal 0.2.5 → 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.
@@ -1,4 +1,4 @@
1
- import { t as APP_VERSION } from "./version-BlgPglaO.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.5";
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"]),
@@ -242,6 +242,8 @@ const requestApi = async (endpoint, options = {}) => {
242
242
  };
243
243
  if (options.token) headers.Authorization = `Bearer ${options.token}`;
244
244
  if (options.oidcToken) headers["X-GitHub-OIDC"] = options.oidcToken;
245
+ const orgId = options.orgId || process.env.RAIGAL_ORG_ID;
246
+ if (orgId) headers["X-Raigal-Org-Id"] = orgId;
245
247
  let body;
246
248
  if (options.body !== void 0) {
247
249
  headers["Content-Type"] = "application/json";
@@ -273,11 +275,12 @@ const requestApi = async (endpoint, options = {}) => {
273
275
  }
274
276
  return json;
275
277
  };
276
- const fetchEntitlementFromApi = async (token, oidcToken) => {
278
+ const fetchEntitlementFromApi = async (token, oidcToken, orgId) => {
277
279
  const data = await requestApi("/v1/entitlement", {
278
280
  method: "POST",
279
281
  token: token || void 0,
280
- oidcToken
282
+ oidcToken,
283
+ orgId
281
284
  });
282
285
  return EntitlementResponse.parse(data);
283
286
  };
@@ -290,14 +293,6 @@ const fetchEntitlementFromApi = async (token, oidcToken) => {
290
293
  const EMBEDDED_PUBLIC_KEYS = { rgl_key_2026_1: "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA69B+1w8c4yJpEURW4Ps2b4BZcoIzeBg3jkCfIo2n2eM=\n-----END PUBLIC KEY-----" };
291
294
  const keyObjectCache = /* @__PURE__ */ new Map();
292
295
  const getPublicKey = (kid) => {
293
- if (process.env.RAIGAL_KEY_ID === kid && process.env.RAIGAL_PUBLIC_KEY) {
294
- const customKey = process.env.RAIGAL_PUBLIC_KEY.replace(/\\n/g, "\n");
295
- try {
296
- return createPublicKey(customKey);
297
- } catch {
298
- return null;
299
- }
300
- }
301
296
  const cached = keyObjectCache.get(kid);
302
297
  if (cached) return cached;
303
298
  const pem = EMBEDDED_PUBLIC_KEYS[kid];
@@ -357,9 +352,8 @@ const verifyEntitlementJwt = (jwt, options = {}) => {
357
352
  const parsed = EntitlementClaims.safeParse(rawPayload);
358
353
  if (!parsed.success) throw new JwtVerificationError(`Invalid entitlement claims: ${parsed.error.message}`, "invalid_claims");
359
354
  const claims = parsed.data;
360
- const expectedIssuer = process.env.RAIGAL_ISSUER?.trim() || "https://app.raigal.dev";
361
- if (claims.iss !== expectedIssuer) throw new JwtVerificationError(`Untrusted issuer: ${claims.iss}`, "invalid_issuer");
362
- if (!isVersionAtLeast("0.2.5", 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");
363
357
  const now = options.nowSeconds ?? Math.floor(Date.now() / 1e3);
364
358
  const grace = options.allowGraceSeconds ?? 0;
365
359
  if (now > claims.exp + grace) {
@@ -518,7 +512,9 @@ const getEntitlement = async () => {
518
512
  }
519
513
  } else oidcToken = await fetchGitHubOidcToken();
520
514
  try {
521
- const response = await fetchEntitlementFromApi(activeToken?.token ?? "", oidcToken);
515
+ const tokenToPass = activeToken?.token ?? "";
516
+ const orgId = process.env.RAIGAL_ORG_ID;
517
+ const response = await fetchEntitlementFromApi(tokenToPass, oidcToken, orgId);
522
518
  const claims = verifyEntitlementJwt(response.entitlement, { nowSeconds: now });
523
519
  saveCachedEntitlement(response.entitlement, response.refresh_after);
524
520
  return {
@@ -532,7 +528,7 @@ const getEntitlement = async () => {
532
528
  };
533
529
  //#endregion
534
530
  //#region src/cloud/owner.ts
535
- const GITHUB_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)github\.com(?::(?:\d+\/)?|[:/])|https:\/\/(?:[^@]+@)?github\.com\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
531
+ const GENERIC_GIT_REMOTE_RE = /^(?:(?:git@|ssh:\/\/(?:git@)?)[^:/]+(?::(?:\d+\/)?|[:/])|https?:\/\/(?:[^@]+@)?[^/]+\/)([^/]+)\/([^/\s]+?)(?:\.git)?\s*$/;
536
532
  const parseGitRemote = (rawUrl) => {
537
533
  if (!rawUrl) return {
538
534
  owner: null,
@@ -540,18 +536,19 @@ const parseGitRemote = (rawUrl) => {
540
536
  hasRemote: false,
541
537
  isGitHubRemote: false
542
538
  };
543
- const match = rawUrl.match(GITHUB_REMOTE_RE);
539
+ const match = rawUrl.match(GENERIC_GIT_REMOTE_RE);
544
540
  if (!match || !match[1] || !match[2]) return {
545
541
  owner: null,
546
542
  repo: null,
547
543
  hasRemote: true,
548
544
  isGitHubRemote: false
549
545
  };
546
+ const isGitHub = rawUrl.includes("github.com");
550
547
  return {
551
548
  owner: match[1].toLowerCase(),
552
549
  repo: match[2].toLowerCase(),
553
550
  hasRemote: true,
554
- isGitHubRemote: true
551
+ isGitHubRemote: isGitHub
555
552
  };
556
553
  };
557
554
  const readGitConfigRemote = (dir) => {
@@ -616,6 +613,27 @@ const detectLocalRepoOwner = (directory) => {
616
613
  isGitHubRemote: true
617
614
  };
618
615
  }
616
+ const gitlabRepo = process.env.CI_PROJECT_PATH?.trim();
617
+ if (gitlabRepo && gitlabRepo.includes("/")) {
618
+ const parts = gitlabRepo.split("/");
619
+ const repo = parts.pop() ?? "";
620
+ return {
621
+ owner: (parts[0] ?? "").toLowerCase(),
622
+ repo: repo.toLowerCase(),
623
+ hasRemote: true,
624
+ isGitHubRemote: false
625
+ };
626
+ }
627
+ const bitbucketRepo = (process.env.BITBUCKET_REPO_FULL_NAME || process.env.BITBUCKET_REPO_SLUG)?.trim();
628
+ if (bitbucketRepo) {
629
+ const [owner = process.env.BITBUCKET_WORKSPACE || "", repo = ""] = bitbucketRepo.includes("/") ? bitbucketRepo.split("/") : [process.env.BITBUCKET_WORKSPACE || "", bitbucketRepo];
630
+ return {
631
+ owner: owner.toLowerCase(),
632
+ repo: repo.toLowerCase(),
633
+ hasRemote: true,
634
+ isGitHubRemote: false
635
+ };
636
+ }
619
637
  const gitRoot = findGitRoot$1(directory);
620
638
  if (!gitRoot) return {
621
639
  owner: null,
@@ -627,26 +645,17 @@ const detectLocalRepoOwner = (directory) => {
627
645
  if (rawUrl !== null) return parseGitRemote(rawUrl);
628
646
  return parseGitRemote(spawnGitRemote(gitRoot));
629
647
  };
630
- const isNonGitHubCi = () => {
631
- const env = process.env;
632
- if (!(env.CI === "true" || env.CI === "1")) return false;
633
- return Boolean(env.GITLAB_CI || env.BITBUCKET_BUILD_NUMBER || env.CIRCLECI && !env.CIRCLE_PROJECT_REPONAME?.includes("/") || env.BUILDKITE || env.TRAVIS || env.JENKINS_URL);
634
- };
635
648
  const checkOwnerAgainstAllowlist = (detected, allowedOwners, tokenKind) => {
636
- if (isNonGitHubCi()) return {
637
- allowed: false,
638
- reason: "Non-GitHub CI environments are unsupported under organization allowlists."
639
- };
640
649
  if (!detected.hasRemote) {
641
650
  if (tokenKind === "session") return { allowed: true };
642
651
  return {
643
652
  allowed: false,
644
- reason: "Repository has no GitHub remote. CI and API key runs require an origin remote matching an allowed organization."
653
+ reason: "Repository has no origin remote. CI and API key runs require an origin remote matching an allowed organization."
645
654
  };
646
655
  }
647
- if (!detected.isGitHubRemote || !detected.owner) return {
656
+ if (!detected.owner) return {
648
657
  allowed: false,
649
- reason: "Repository remote is not hosted on GitHub or could not be determined."
658
+ reason: "Repository remote owner could not be determined."
650
659
  };
651
660
  const lowerOwner = detected.owner.toLowerCase();
652
661
  if (!allowedOwners.some((allowed) => allowed.login.toLowerCase() === lowerOwner)) {
@@ -654,7 +663,7 @@ const checkOwnerAgainstAllowlist = (detected, allowedOwners, tokenKind) => {
654
663
  return {
655
664
  allowed: false,
656
665
  owner: lowerOwner,
657
- reason: `GitHub owner '${detected.owner}' is not on the organization allowlist (${allowedLogins || "none"}).`
666
+ reason: `Repository owner '${detected.owner}' is not on the organization allowlist (${allowedLogins || "none"}).`
658
667
  };
659
668
  }
660
669
  return {
@@ -1854,7 +1863,28 @@ const isSafeRegularProjectFile = (rootDirectory, absolutePath) => {
1854
1863
  return false;
1855
1864
  }
1856
1865
  };
1857
- 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
+ };
1858
1888
  const isInExcludedDirectory = (filePath, excludedDirectories) => {
1859
1889
  const normalized = filePath.toLowerCase();
1860
1890
  return excludedDirectories.some((directory) => normalized === directory || normalized.startsWith(`${directory}/`) || normalized.includes(`/${directory}/`));
@@ -13778,13 +13808,19 @@ const SECRET_PATTERNS = [
13778
13808
  },
13779
13809
  {
13780
13810
  pattern: /AKIA[0-9A-Z]{16}/g,
13781
- name: "AWS Access Key"
13811
+ name: "AWS Access Key",
13812
+ specific: true
13782
13813
  },
13783
13814
  {
13784
- 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,
13785
13816
  name: "AWS Secret Key",
13786
13817
  keywordPrefixed: true
13787
13818
  },
13819
+ {
13820
+ pattern: /\b(?:sk|rk)_live_[A-Za-z0-9]{20,}/g,
13821
+ name: "Stripe secret key",
13822
+ specific: true
13823
+ },
13788
13824
  {
13789
13825
  pattern: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/gi,
13790
13826
  name: "Hardcoded password/secret",
@@ -13792,11 +13828,13 @@ const SECRET_PATTERNS = [
13792
13828
  },
13793
13829
  {
13794
13830
  pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g,
13795
- name: "Private key"
13831
+ name: "Private key",
13832
+ specific: true
13796
13833
  },
13797
13834
  {
13798
13835
  pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
13799
- name: "JWT token"
13836
+ name: "JWT token",
13837
+ specific: true
13800
13838
  },
13801
13839
  {
13802
13840
  pattern: /(?:token|bearer)\s*[:=]\s*["']([A-Za-z0-9_-]{20,})["']/gi,
@@ -13805,17 +13843,51 @@ const SECRET_PATTERNS = [
13805
13843
  },
13806
13844
  {
13807
13845
  pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g,
13808
- name: "GitHub token"
13846
+ name: "GitHub token",
13847
+ specific: true
13809
13848
  },
13810
13849
  {
13811
13850
  pattern: /xox[baprs]-[A-Za-z0-9-]+/g,
13812
- name: "Slack token"
13851
+ name: "Slack token",
13852
+ specific: true
13813
13853
  },
13814
13854
  {
13815
13855
  pattern: /(?:mongodb(?:\+srv)?|postgres|mysql|redis):\/\/[^:@/"'`\s]+:[^@"'`\s]+@[^"'`\s]+/gi,
13816
- name: "Database connection string with credentials"
13856
+ name: "Database connection string with credentials",
13857
+ specific: true
13817
13858
  }
13818
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);
13819
13891
  const isInsideStringLiteral = (content, matchIndex) => {
13820
13892
  const lineStart = content.lastIndexOf("\n", matchIndex - 1) + 1;
13821
13893
  const prefix = content.slice(lineStart, matchIndex);
@@ -13960,16 +14032,69 @@ const shouldSkipSecretFinding = (relativePath, name, lineText, matchedText) => {
13960
14032
  if ((name === "Hardcoded password/secret" || name === "Authentication token") && isSymbolicConstantValue(matchedText)) return true;
13961
14033
  return false;
13962
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
+ };
13963
14095
  const scanSecrets = async (context) => {
13964
- const files = getSourceFilesWithExtras(context, [
13965
- ".env",
13966
- ".yaml",
13967
- ".yml",
13968
- ".json",
13969
- ".toml"
13970
- ]);
13971
14096
  const diagnostics = [];
13972
- for (const filePath of files) {
14097
+ for (const filePath of resolveSecretFiles(context)) {
13973
14098
  let content;
13974
14099
  try {
13975
14100
  content = fs.readFileSync(filePath, "utf-8");
@@ -13978,30 +14103,20 @@ const scanSecrets = async (context) => {
13978
14103
  }
13979
14104
  content = maskComments(content, path.extname(filePath));
13980
14105
  const relativePath = relativePosix(context.rootDirectory, filePath);
13981
- for (const { pattern, name, keywordPrefixed } of SECRET_PATTERNS) {
13982
- const regex = new RegExp(pattern.source, pattern.flags);
13983
- for (const match of content.matchAll(regex)) {
13984
- const matchedText = match[1] ?? match[0];
13985
- const lineText = lineForMatch(content, match.index);
13986
- if (isPlaceholderValue(matchedText)) continue;
13987
- if (shouldSkipSecretFinding(relativePath, name, lineText, matchedText)) continue;
13988
- if (keywordPrefixed && isInsideStringLiteral(content, match.index)) continue;
13989
- const line = content.slice(0, match.index).split("\n").length;
13990
- diagnostics.push({
13991
- filePath: relativePath,
13992
- engine: "security",
13993
- rule: "security/hardcoded-secret",
13994
- severity: "error",
13995
- message: `Possible ${name} detected in source code`,
13996
- help: "Move secrets to environment variables or a secrets manager",
13997
- line,
13998
- column: 0,
13999
- category: "Security",
14000
- fixable: false,
14001
- redactSource: true
14002
- });
14003
- }
14004
- }
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
+ });
14005
14120
  }
14006
14121
  return diagnostics;
14007
14122
  };
@@ -1,4 +1,4 @@
1
- import { t as APP_VERSION } from "./version-BlgPglaO.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
@@ -1,6 +1,6 @@
1
1
  import "node:fs";
2
2
  import "node:url";
3
3
  //#region src/version.ts
4
- const APP_VERSION = "0.2.5";
4
+ const APP_VERSION = "0.2.7";
5
5
  //#endregion
6
6
  export { APP_VERSION as t };
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@methiu/raigal",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "description": "Catch the slop AI coding agents leave in your code: narrative comments, swallowed exceptions, as-any casts, dead code, oversized functions. 50+ rules across 10 language targets (TypeScript, JavaScript, Expo / React Native, Python, Go, Rust, Ruby, PHP, C#, C/C++). Sub-second, deterministic, no LLM at runtime.",
7
+ "description": "Raigal CLI: a code-quality and security gate for AI-written code. Runs formatting, lint, code-quality, AI-pattern, architecture and security checks across 10 language targets and scores each change from 0 to 100.",
8
8
  "type": "module",
9
9
  "bin": {
10
10
  "raigal": "dist/cli.js",
@@ -14,7 +14,9 @@
14
14
  "files": [
15
15
  "dist",
16
16
  "scripts",
17
- "tools/jb"
17
+ "tools/jb",
18
+ "LICENSE",
19
+ "THIRD_PARTY_NOTICES.md"
18
20
  ],
19
21
  "exports": {
20
22
  ".": {
@@ -81,7 +83,8 @@
81
83
  "cpp"
82
84
  ],
83
85
  "author": {
84
- "name": "Kenny Olawuwo",
86
+ "name": "Raigal",
87
+ "email": "matei@raigal.dev",
85
88
  "url": "https://raigal.dev"
86
89
  },
87
90
  "license": "SEE LICENSE IN LICENSE",
@@ -11,6 +11,15 @@ const ALLOWED_KEYS = new Set([]);
11
11
  const PH_KEY_PREFIX = `${"p" + "h" + "c"}_`;
12
12
  const PH_KEY_PATTERN = new RegExp(`${PH_KEY_PREFIX}[A-Za-z0-9_-]{20,}`, "g");
13
13
 
14
+ // Signing-key and issuer overrides must never reach the published package.
15
+ const FORBIDDEN_STRINGS = [
16
+ "RAIGAL_PUBLIC_KEY",
17
+ "RAIGAL_KEY_ID",
18
+ "RAIGAL_ISSUER",
19
+ "RAIGAL_TEST_PUBLIC_KEY",
20
+ "RAIGAL_TEST_KEY_ID",
21
+ ];
22
+
14
23
  const ROOT_DIR = path.resolve(import.meta.dirname, "..");
15
24
 
16
25
  const walkDirectory = (dir, callback) => {
@@ -60,11 +69,20 @@ try {
60
69
 
61
70
  // 3. Scan all extracted files for unapproved PostHog keys
62
71
  const unauthorizedKeys = [];
72
+ const forbiddenHits = [];
63
73
 
64
74
  walkDirectory(packageDir, (filePath) => {
65
75
  // Only check text / script / metadata files
66
76
  try {
67
77
  const content = fs.readFileSync(filePath, "utf-8");
78
+ const relFile = path.relative(packageDir, filePath).split(path.sep).join("/");
79
+ if (relFile.startsWith("dist/")) {
80
+ for (const needle of FORBIDDEN_STRINGS) {
81
+ if (content.includes(needle)) {
82
+ forbiddenHits.push({ needle, file: relFile });
83
+ }
84
+ }
85
+ }
68
86
  const matches = content.match(PH_KEY_PATTERN);
69
87
  if (matches) {
70
88
  for (const match of matches) {
@@ -87,6 +105,14 @@ try {
87
105
  process.exit(1);
88
106
  }
89
107
 
108
+ if (forbiddenHits.length > 0) {
109
+ console.error("Tarball key check FAILED: signing-key overrides found in package:");
110
+ for (const item of forbiddenHits) {
111
+ console.error(` - "${item.needle}" in file "${item.file}"`);
112
+ }
113
+ process.exit(1);
114
+ }
115
+
90
116
  console.log("Tarball key check passed: No unauthorized PostHog keys found in package.");
91
117
  process.exit(0);
92
118
  } catch (err) {