@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.
- package/LICENSE +6 -1
- package/README.md +75 -37
- package/THIRD_PARTY_NOTICES.md +70 -0
- package/dist/cli.js +588 -185
- package/dist/index.d.ts +1 -0
- package/dist/index.js +536 -177
- package/dist/{json-B8XGYwMv.js → json-B2Kc7_rw.js} +1 -1
- package/dist/mcp.js +187 -72
- package/dist/{sarif-DqH4dGEo.js → sarif-CpMse0iL.js} +1 -1
- package/dist/{version-BlgPglaO.js → version-CXMYu17z.js} +1 -1
- package/package.json +7 -4
- package/scripts/check-tarball-keys.mjs +26 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as APP_VERSION } from "./version-
|
|
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.
|
|
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(
|
|
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
|
-
|
|
361
|
-
if (claims.
|
|
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
|
|
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
|
|
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(
|
|
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:
|
|
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
|
|
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.
|
|
656
|
+
if (!detected.owner) return {
|
|
648
657
|
allowed: false,
|
|
649
|
-
reason: "Repository remote
|
|
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: `
|
|
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
|
|
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
|
|
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
|
-
|
|
13982
|
-
|
|
13983
|
-
|
|
13984
|
-
|
|
13985
|
-
|
|
13986
|
-
|
|
13987
|
-
|
|
13988
|
-
|
|
13989
|
-
|
|
13990
|
-
|
|
13991
|
-
|
|
13992
|
-
|
|
13993
|
-
|
|
13994
|
-
|
|
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-
|
|
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
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@methiu/raigal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"description": "
|
|
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": "
|
|
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) {
|