@biffo/cli 0.267.2 → 0.267.4
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 +764 -120
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -377,12 +377,12 @@ function toPosix(p) {
|
|
|
377
377
|
function listTemplateOwnedFiles(root, manifest, options = {}) {
|
|
378
378
|
const out = [];
|
|
379
379
|
const tracked = options.trackedOnly ? options.git ? gitTrackedFiles(root, options.git) : gitTrackedFiles(root) : null;
|
|
380
|
-
function
|
|
380
|
+
function walk2(dir) {
|
|
381
381
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
382
382
|
if (entry.isDirectory() && HARD_EXCLUDED_DIRS.has(entry.name)) continue;
|
|
383
383
|
const abs = join2(dir, entry.name);
|
|
384
384
|
if (entry.isDirectory()) {
|
|
385
|
-
|
|
385
|
+
walk2(abs);
|
|
386
386
|
} else if (entry.isFile()) {
|
|
387
387
|
const rel = toPosix(relative(root, abs));
|
|
388
388
|
if (tracked && !tracked.has(rel)) continue;
|
|
@@ -390,7 +390,7 @@ function listTemplateOwnedFiles(root, manifest, options = {}) {
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
}
|
|
393
|
-
|
|
393
|
+
walk2(root);
|
|
394
394
|
return out.sort();
|
|
395
395
|
}
|
|
396
396
|
function sameFile(a, b) {
|
|
@@ -6030,12 +6030,12 @@ function formatStaleBuildError(result) {
|
|
|
6030
6030
|
}
|
|
6031
6031
|
function collectSourceFiles(srcDir) {
|
|
6032
6032
|
const found = [];
|
|
6033
|
-
const
|
|
6033
|
+
const walk2 = (dir) => {
|
|
6034
6034
|
for (const entry of readdirSync7(dir, { withFileTypes: true })) {
|
|
6035
6035
|
const full = join19(dir, entry.name);
|
|
6036
6036
|
if (entry.isDirectory()) {
|
|
6037
6037
|
if (entry.name === "node_modules") continue;
|
|
6038
|
-
|
|
6038
|
+
walk2(full);
|
|
6039
6039
|
continue;
|
|
6040
6040
|
}
|
|
6041
6041
|
if (!entry.isFile()) continue;
|
|
@@ -6045,7 +6045,7 @@ function collectSourceFiles(srcDir) {
|
|
|
6045
6045
|
found.push(full);
|
|
6046
6046
|
}
|
|
6047
6047
|
};
|
|
6048
|
-
|
|
6048
|
+
walk2(srcDir);
|
|
6049
6049
|
return found;
|
|
6050
6050
|
}
|
|
6051
6051
|
function findPackageRoot(from) {
|
|
@@ -7834,7 +7834,7 @@ function scaffoldPlugin(skeletonRoot, destDir, names, options = {}) {
|
|
|
7834
7834
|
}
|
|
7835
7835
|
const skipped = [];
|
|
7836
7836
|
const files = [];
|
|
7837
|
-
const
|
|
7837
|
+
const walk2 = (relDir) => {
|
|
7838
7838
|
const absDir = join25(skeletonRoot, relDir);
|
|
7839
7839
|
for (const entry of readdirSync11(absDir, { withFileTypes: true }).sort(
|
|
7840
7840
|
(a, b) => a.name.localeCompare(b.name)
|
|
@@ -7846,7 +7846,7 @@ function scaffoldPlugin(skeletonRoot, destDir, names, options = {}) {
|
|
|
7846
7846
|
}
|
|
7847
7847
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
7848
7848
|
if (entry.isDirectory()) {
|
|
7849
|
-
|
|
7849
|
+
walk2(relPath);
|
|
7850
7850
|
continue;
|
|
7851
7851
|
}
|
|
7852
7852
|
const destRel = applySubstitutions(relPath, names);
|
|
@@ -7863,7 +7863,7 @@ function scaffoldPlugin(skeletonRoot, destDir, names, options = {}) {
|
|
|
7863
7863
|
files.push(destRel);
|
|
7864
7864
|
}
|
|
7865
7865
|
};
|
|
7866
|
-
|
|
7866
|
+
walk2("");
|
|
7867
7867
|
if (!files.some((f) => f.startsWith("terraform/"))) {
|
|
7868
7868
|
throw new Error(
|
|
7869
7869
|
`Scaffold produced no terraform/ files from ${skeletonRoot} \u2014 refusing to leave a plugin whose event subscriptions could never fire (issue #194).`
|
|
@@ -9872,13 +9872,142 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
|
|
|
9872
9872
|
console.log(`\u2713 branch-protection guard: ${owner}/${repo} (${audited.join(", ")}) OK`);
|
|
9873
9873
|
}
|
|
9874
9874
|
|
|
9875
|
-
// src/scripts/check-
|
|
9876
|
-
import { join as join37 } from "path";
|
|
9875
|
+
// src/scripts/check-cognito-invite-template.ts
|
|
9877
9876
|
import { execa as execa7 } from "execa";
|
|
9878
9877
|
|
|
9879
|
-
// src/lib/
|
|
9880
|
-
import {
|
|
9878
|
+
// src/lib/cognito-invite-template-guard.ts
|
|
9879
|
+
import { readdirSync as readdirSync14, readFileSync as readFileSync27, statSync as statSync7 } from "fs";
|
|
9881
9880
|
import { join as join36 } from "path";
|
|
9881
|
+
var REQUIRED_INVITE_MEMBERS = ["email_subject", "email_message", "sms_message"];
|
|
9882
|
+
var REQUIRED_INVITE_PLACEHOLDERS = ["{username}", "{####}"];
|
|
9883
|
+
var PLACEHOLDER_MEMBERS = ["email_message", "sms_message"];
|
|
9884
|
+
function stripHeredocs(source) {
|
|
9885
|
+
return source.replace(/<<-?(\w+)\r?\n[\s\S]*?\r?\n[ \t]*\1\b/g, '"HEREDOC"');
|
|
9886
|
+
}
|
|
9887
|
+
function stripHclComments(source) {
|
|
9888
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, "").split("\n").map((line) => line.replace(/(^|\s)(#|\/\/).*$/, "$1")).join("\n");
|
|
9889
|
+
}
|
|
9890
|
+
function extractBlocks(source, name) {
|
|
9891
|
+
const blocks = [];
|
|
9892
|
+
const opener = new RegExp(`(?:^|[\\s])${name}\\s*\\{`, "g");
|
|
9893
|
+
let match;
|
|
9894
|
+
while ((match = opener.exec(source)) !== null) {
|
|
9895
|
+
const openBrace = source.indexOf("{", match.index);
|
|
9896
|
+
let depth = 1;
|
|
9897
|
+
let i = openBrace + 1;
|
|
9898
|
+
for (; i < source.length && depth > 0; i++) {
|
|
9899
|
+
if (source[i] === "{") depth++;
|
|
9900
|
+
else if (source[i] === "}") depth--;
|
|
9901
|
+
}
|
|
9902
|
+
blocks.push({
|
|
9903
|
+
line: source.slice(0, match.index).split("\n").length,
|
|
9904
|
+
body: source.slice(openBrace + 1, i - 1)
|
|
9905
|
+
});
|
|
9906
|
+
}
|
|
9907
|
+
return blocks;
|
|
9908
|
+
}
|
|
9909
|
+
function checkInviteTemplateSource(file, rawSource) {
|
|
9910
|
+
const source = stripHclComments(stripHeredocs(rawSource));
|
|
9911
|
+
const violations = [];
|
|
9912
|
+
for (const block of extractBlocks(source, "invite_message_template")) {
|
|
9913
|
+
for (const member of REQUIRED_INVITE_MEMBERS) {
|
|
9914
|
+
const assigned = new RegExp(`(?:^|[\\s{])${member}\\s*=`).test(block.body);
|
|
9915
|
+
if (!assigned) {
|
|
9916
|
+
violations.push({
|
|
9917
|
+
file,
|
|
9918
|
+
line: block.line,
|
|
9919
|
+
message: `invite_message_template is missing "${member}". Cognito's CreateUserPool validates all of ${REQUIRED_INVITE_MEMBERS.join(", ")} and rejects an empty member with InvalidParameterException ("length greater than or equal to 6"), so no user pool can be created and every fresh deploy fails (issue #356).`
|
|
9920
|
+
});
|
|
9921
|
+
}
|
|
9922
|
+
}
|
|
9923
|
+
}
|
|
9924
|
+
for (const block of extractBlocks(rawSource, "invite_message_template")) {
|
|
9925
|
+
for (const member of PLACEHOLDER_MEMBERS) {
|
|
9926
|
+
const body = memberBody(block.body, member);
|
|
9927
|
+
if (body === null) continue;
|
|
9928
|
+
for (const placeholder of REQUIRED_INVITE_PLACEHOLDERS) {
|
|
9929
|
+
if (!body.includes(placeholder)) {
|
|
9930
|
+
violations.push({
|
|
9931
|
+
file,
|
|
9932
|
+
line: block.line,
|
|
9933
|
+
message: `invite_message_template's "${member}" is missing the ${placeholder} placeholder. Cognito's CreateUserPool rejects the template outright ("${member === "sms_message" ? "SMS message" : "Email message body"} should have ${placeholder} which will be replaced by code"), so no user pool can be created. Note {username} is required even when the pool uses username_attributes and the value is an opaque UUID \u2014 keep it, but do not ask the recipient to type it.`
|
|
9934
|
+
});
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
}
|
|
9938
|
+
}
|
|
9939
|
+
return violations;
|
|
9940
|
+
}
|
|
9941
|
+
function memberBody(blockBody, member) {
|
|
9942
|
+
const heredoc = new RegExp(
|
|
9943
|
+
`(?:^|[\\s{])${member}\\s*=\\s*<<-?([A-Za-z_]+)([\\s\\S]*?)^\\s*\\1`,
|
|
9944
|
+
"m"
|
|
9945
|
+
);
|
|
9946
|
+
const heredocMatch = heredoc.exec(blockBody);
|
|
9947
|
+
if (heredocMatch?.[2] !== void 0) return heredocMatch[2];
|
|
9948
|
+
const quoted = new RegExp(`(?:^|[\\s{])${member}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"`);
|
|
9949
|
+
const quotedMatch = quoted.exec(blockBody);
|
|
9950
|
+
return quotedMatch?.[1] ?? null;
|
|
9951
|
+
}
|
|
9952
|
+
function findModuleTerraformFiles(repoRoot) {
|
|
9953
|
+
const found = [];
|
|
9954
|
+
const walk2 = (dir, relative8) => {
|
|
9955
|
+
let entries;
|
|
9956
|
+
try {
|
|
9957
|
+
entries = readdirSync14(dir);
|
|
9958
|
+
} catch {
|
|
9959
|
+
return;
|
|
9960
|
+
}
|
|
9961
|
+
for (const entry of entries) {
|
|
9962
|
+
if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
|
|
9963
|
+
const full = join36(dir, entry);
|
|
9964
|
+
const rel = `${relative8}/${entry}`;
|
|
9965
|
+
if (statSync7(full).isDirectory()) {
|
|
9966
|
+
walk2(full, rel);
|
|
9967
|
+
} else if (entry.endsWith(".tf")) {
|
|
9968
|
+
found.push(rel);
|
|
9969
|
+
}
|
|
9970
|
+
}
|
|
9971
|
+
};
|
|
9972
|
+
walk2(join36(repoRoot, "modules"), "modules");
|
|
9973
|
+
return found.sort();
|
|
9974
|
+
}
|
|
9975
|
+
function checkCognitoInviteTemplates(repoRoot) {
|
|
9976
|
+
return findModuleTerraformFiles(repoRoot).flatMap(
|
|
9977
|
+
(file) => checkInviteTemplateSource(file, readFileSync27(join36(repoRoot, file), "utf8"))
|
|
9978
|
+
);
|
|
9979
|
+
}
|
|
9980
|
+
|
|
9981
|
+
// src/scripts/check-cognito-invite-template.ts
|
|
9982
|
+
async function runCognitoInviteTemplateCheck() {
|
|
9983
|
+
const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
9984
|
+
const files = findModuleTerraformFiles(root);
|
|
9985
|
+
console.log(`audited ${files.length} .tf file(s) under modules/ under ${root}`);
|
|
9986
|
+
if (files.length === 0) {
|
|
9987
|
+
console.error(
|
|
9988
|
+
"\u2717 Cognito invite template guard: found 0 .tf files under modules/ \u2014 this looks like a broken scan, not a clean repo. Refusing to report success over zero input."
|
|
9989
|
+
);
|
|
9990
|
+
process.exit(1);
|
|
9991
|
+
}
|
|
9992
|
+
const violations = checkCognitoInviteTemplates(root);
|
|
9993
|
+
if (violations.length > 0) {
|
|
9994
|
+
console.error("\u2717 Cognito invite template guard: incomplete invite_message_template(s)\n");
|
|
9995
|
+
for (const v of violations) {
|
|
9996
|
+
console.error(` ${v.file}:${v.line} ${v.message}`);
|
|
9997
|
+
}
|
|
9998
|
+
console.error("\nSee biffo-template#356.");
|
|
9999
|
+
process.exit(1);
|
|
10000
|
+
}
|
|
10001
|
+
console.log(`\u2713 Cognito invite template guard: every invite_message_template block is complete`);
|
|
10002
|
+
}
|
|
10003
|
+
|
|
10004
|
+
// src/scripts/check-core-direct-paths.ts
|
|
10005
|
+
import { join as join38 } from "path";
|
|
10006
|
+
import { execa as execa8 } from "execa";
|
|
10007
|
+
|
|
10008
|
+
// src/lib/core-direct-paths-audit.ts
|
|
10009
|
+
import { readFileSync as readFileSync28, readdirSync as readdirSync15, statSync as statSync8 } from "fs";
|
|
10010
|
+
import { join as join37 } from "path";
|
|
9882
10011
|
var EXTERNAL_BASE_IDENTIFIERS = ["CORE_API_URL"];
|
|
9883
10012
|
var API_ROUTE_PREFIX = "/api/v1";
|
|
9884
10013
|
var TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
|
|
@@ -10034,30 +10163,30 @@ function countRawExternalOccurrences(rawText, externalBases = EXTERNAL_BASE_IDEN
|
|
|
10034
10163
|
}
|
|
10035
10164
|
function walkFiles(root, accept, skipDir) {
|
|
10036
10165
|
const out = [];
|
|
10037
|
-
const
|
|
10166
|
+
const walk2 = (dir) => {
|
|
10038
10167
|
let entries;
|
|
10039
10168
|
try {
|
|
10040
|
-
entries =
|
|
10169
|
+
entries = readdirSync15(dir);
|
|
10041
10170
|
} catch {
|
|
10042
10171
|
return;
|
|
10043
10172
|
}
|
|
10044
10173
|
for (const entry of entries) {
|
|
10045
|
-
const p =
|
|
10174
|
+
const p = join37(dir, entry);
|
|
10046
10175
|
let st;
|
|
10047
10176
|
try {
|
|
10048
|
-
st =
|
|
10177
|
+
st = statSync8(p);
|
|
10049
10178
|
} catch {
|
|
10050
10179
|
continue;
|
|
10051
10180
|
}
|
|
10052
10181
|
if (st.isDirectory()) {
|
|
10053
10182
|
if (skipDir(entry)) continue;
|
|
10054
|
-
|
|
10183
|
+
walk2(p);
|
|
10055
10184
|
continue;
|
|
10056
10185
|
}
|
|
10057
10186
|
if (accept(entry)) out.push(p);
|
|
10058
10187
|
}
|
|
10059
10188
|
};
|
|
10060
|
-
|
|
10189
|
+
walk2(root);
|
|
10061
10190
|
return out.sort();
|
|
10062
10191
|
}
|
|
10063
10192
|
function frontendSourceFiles(frontendSrcDir) {
|
|
@@ -10072,7 +10201,7 @@ function auditFrontendExtraction(frontendSrcDir, externalBases = EXTERNAL_BASE_I
|
|
|
10072
10201
|
const extracted = [];
|
|
10073
10202
|
let rawTotal = 0;
|
|
10074
10203
|
for (const file of files) {
|
|
10075
|
-
const text =
|
|
10204
|
+
const text = readFileSync28(file, "utf8");
|
|
10076
10205
|
rawTotal += countRawExternalOccurrences(text, externalBases);
|
|
10077
10206
|
extracted.push(...extractCoreDirectPaths(text, file, externalBases));
|
|
10078
10207
|
}
|
|
@@ -10121,7 +10250,7 @@ function auditCoreRouteExtraction(apiSrcDir) {
|
|
|
10121
10250
|
const prefixSet = /* @__PURE__ */ new Set();
|
|
10122
10251
|
let rawApiRouterCount = 0;
|
|
10123
10252
|
for (const file of files) {
|
|
10124
|
-
const text =
|
|
10253
|
+
const text = readFileSync28(file, "utf8");
|
|
10125
10254
|
const extraction = extractCoreRoutePrefixes(text);
|
|
10126
10255
|
rawApiRouterCount += extraction.rawApiRouterCount;
|
|
10127
10256
|
for (const p of extraction.prefixes) prefixSet.add(normalizePrefix(p));
|
|
@@ -10169,10 +10298,10 @@ function auditSiblingCoreDirectPaths(params) {
|
|
|
10169
10298
|
|
|
10170
10299
|
// src/scripts/check-core-direct-paths.ts
|
|
10171
10300
|
async function runCoreDirectPathsCheck(opts = {}) {
|
|
10172
|
-
const root = (await
|
|
10301
|
+
const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10173
10302
|
const sibling = opts.sibling ?? "sibling-template (self-check)";
|
|
10174
|
-
const frontendSrcDir = opts.frontendSrc ??
|
|
10175
|
-
const coreApiSrcDir = opts.coreSrc ??
|
|
10303
|
+
const frontendSrcDir = opts.frontendSrc ?? join38(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
|
|
10304
|
+
const coreApiSrcDir = opts.coreSrc ?? join38(root, "services", "api", "src");
|
|
10176
10305
|
const report = auditSiblingCoreDirectPaths({ sibling, frontendSrcDir, coreApiSrcDir });
|
|
10177
10306
|
console.log(
|
|
10178
10307
|
`audited ${report.extractedCount} core-direct call site(s) across ${report.frontendFiles} frontend file(s) under ${frontendSrcDir}, against ${report.corePrefixCount} route prefix(es) from ${report.coreFiles} core file(s) under ${coreApiSrcDir}`
|
|
@@ -10207,7 +10336,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
|
|
|
10207
10336
|
}
|
|
10208
10337
|
|
|
10209
10338
|
// src/scripts/check-core-ownership.ts
|
|
10210
|
-
import { execa as
|
|
10339
|
+
import { execa as execa9 } from "execa";
|
|
10211
10340
|
var BOLD = "\x1B[1m";
|
|
10212
10341
|
var DIM = "\x1B[2m";
|
|
10213
10342
|
var RED = "\x1B[31m";
|
|
@@ -10218,7 +10347,7 @@ async function runOwnershipCheck(argv) {
|
|
|
10218
10347
|
const stagedFlag = args.indexOf("--staged");
|
|
10219
10348
|
const staged = stagedFlag !== -1;
|
|
10220
10349
|
const messageFile = staged ? args[stagedFlag + 1] : void 0;
|
|
10221
|
-
const root = (await
|
|
10350
|
+
const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10222
10351
|
const ownership = classifyRepoOwnership(root);
|
|
10223
10352
|
if (ownership === "template") {
|
|
10224
10353
|
console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
|
|
@@ -10234,11 +10363,11 @@ async function runOwnershipCheck(argv) {
|
|
|
10234
10363
|
let deletedFiles = [];
|
|
10235
10364
|
let commitMessage = "";
|
|
10236
10365
|
if (staged) {
|
|
10237
|
-
const { stdout } = await
|
|
10366
|
+
const { stdout } = await execa9("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
10238
10367
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
10239
10368
|
if (messageFile) {
|
|
10240
|
-
const { readFileSync:
|
|
10241
|
-
if (
|
|
10369
|
+
const { readFileSync: readFileSync37, existsSync: existsSync44 } = await import("fs");
|
|
10370
|
+
if (existsSync44(messageFile)) commitMessage = readFileSync37(messageFile, "utf8");
|
|
10242
10371
|
}
|
|
10243
10372
|
} else {
|
|
10244
10373
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -10246,18 +10375,18 @@ async function runOwnershipCheck(argv) {
|
|
|
10246
10375
|
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
10247
10376
|
process.exit(2);
|
|
10248
10377
|
}
|
|
10249
|
-
await
|
|
10250
|
-
const { stdout } = await
|
|
10378
|
+
await execa9("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
10379
|
+
const { stdout } = await execa9("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
|
|
10251
10380
|
cwd: root
|
|
10252
10381
|
});
|
|
10253
10382
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
10254
|
-
const { stdout: log2 } = await
|
|
10383
|
+
const { stdout: log2 } = await execa9("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
|
|
10255
10384
|
cwd: root,
|
|
10256
10385
|
reject: false
|
|
10257
10386
|
});
|
|
10258
10387
|
commitMessage = log2;
|
|
10259
10388
|
}
|
|
10260
|
-
const { stdout: gitBranch } = await
|
|
10389
|
+
const { stdout: gitBranch } = await execa9("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
10261
10390
|
cwd: root,
|
|
10262
10391
|
reject: false
|
|
10263
10392
|
});
|
|
@@ -10339,11 +10468,11 @@ ${BOLD}If the divergence is deliberate${OFF}
|
|
|
10339
10468
|
}
|
|
10340
10469
|
|
|
10341
10470
|
// src/scripts/check-eventbridge-log-permissions.ts
|
|
10342
|
-
import { execa as
|
|
10471
|
+
import { execa as execa10 } from "execa";
|
|
10343
10472
|
|
|
10344
10473
|
// src/lib/eventbridge-log-permission-guard.ts
|
|
10345
|
-
import { readFileSync as
|
|
10346
|
-
import { join as
|
|
10474
|
+
import { readFileSync as readFileSync29, readdirSync as readdirSync16, statSync as statSync9 } from "fs";
|
|
10475
|
+
import { join as join39 } from "path";
|
|
10347
10476
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".terraform", ".worktrees", "dist"]);
|
|
10348
10477
|
var EVENT_TARGET_TYPE = "aws_cloudwatch_event_target";
|
|
10349
10478
|
var LOG_RESOURCE_POLICY_TYPE = "aws_cloudwatch_log_resource_policy";
|
|
@@ -10412,30 +10541,30 @@ function countRawResourceDeclarations(text, resourceType) {
|
|
|
10412
10541
|
}
|
|
10413
10542
|
function walkTerraformFiles(root) {
|
|
10414
10543
|
const out = [];
|
|
10415
|
-
const
|
|
10544
|
+
const walk2 = (dir) => {
|
|
10416
10545
|
let entries;
|
|
10417
10546
|
try {
|
|
10418
|
-
entries =
|
|
10547
|
+
entries = readdirSync16(dir);
|
|
10419
10548
|
} catch {
|
|
10420
10549
|
return;
|
|
10421
10550
|
}
|
|
10422
10551
|
for (const entry of entries) {
|
|
10423
|
-
const p =
|
|
10552
|
+
const p = join39(dir, entry);
|
|
10424
10553
|
let st;
|
|
10425
10554
|
try {
|
|
10426
|
-
st =
|
|
10555
|
+
st = statSync9(p);
|
|
10427
10556
|
} catch {
|
|
10428
10557
|
continue;
|
|
10429
10558
|
}
|
|
10430
10559
|
if (st.isDirectory()) {
|
|
10431
10560
|
if (SKIP_DIRS.has(entry)) continue;
|
|
10432
|
-
|
|
10561
|
+
walk2(p);
|
|
10433
10562
|
continue;
|
|
10434
10563
|
}
|
|
10435
10564
|
if (entry.endsWith(".tf")) out.push(p);
|
|
10436
10565
|
}
|
|
10437
10566
|
};
|
|
10438
|
-
|
|
10567
|
+
walk2(root);
|
|
10439
10568
|
return out.sort();
|
|
10440
10569
|
}
|
|
10441
10570
|
function resolveEventTargetLogGroup(body) {
|
|
@@ -10463,7 +10592,7 @@ function auditEventBridgeLogPermissions(root) {
|
|
|
10463
10592
|
let rawEventTargetCount = 0;
|
|
10464
10593
|
let rawLogPolicyCount = 0;
|
|
10465
10594
|
for (const file of files) {
|
|
10466
|
-
const text =
|
|
10595
|
+
const text = readFileSync29(file, "utf8");
|
|
10467
10596
|
rawEventTargetCount += countRawResourceDeclarations(text, EVENT_TARGET_TYPE);
|
|
10468
10597
|
rawLogPolicyCount += countRawResourceDeclarations(text, LOG_RESOURCE_POLICY_TYPE);
|
|
10469
10598
|
eventTargetBlocks.push(...findResourceBlocks(text, file, EVENT_TARGET_TYPE));
|
|
@@ -10516,7 +10645,7 @@ function auditEventBridgeLogPermissions(root) {
|
|
|
10516
10645
|
|
|
10517
10646
|
// src/scripts/check-eventbridge-log-permissions.ts
|
|
10518
10647
|
async function runEventBridgeLogPermissionCheck() {
|
|
10519
|
-
const root = (await
|
|
10648
|
+
const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10520
10649
|
let report;
|
|
10521
10650
|
try {
|
|
10522
10651
|
report = auditEventBridgeLogPermissions(root);
|
|
@@ -10552,35 +10681,338 @@ async function runEventBridgeLogPermissionCheck() {
|
|
|
10552
10681
|
console.log(`\u2713 EventBridge log permission guard: ${report.summary}`);
|
|
10553
10682
|
}
|
|
10554
10683
|
|
|
10684
|
+
// src/scripts/check-lambda-output.ts
|
|
10685
|
+
import { execa as execa11 } from "execa";
|
|
10686
|
+
|
|
10687
|
+
// src/lib/lambda-output-guard.ts
|
|
10688
|
+
import { readFileSync as readFileSync31 } from "fs";
|
|
10689
|
+
import { join as join41 } from "path";
|
|
10690
|
+
|
|
10691
|
+
// src/lib/terraform-input-guard.ts
|
|
10692
|
+
import { readdirSync as readdirSync17, readFileSync as readFileSync30, statSync as statSync10 } from "fs";
|
|
10693
|
+
import { join as join40 } from "path";
|
|
10694
|
+
var GUARDED_SUBCOMMANDS = [
|
|
10695
|
+
"init",
|
|
10696
|
+
"plan",
|
|
10697
|
+
"apply",
|
|
10698
|
+
"destroy",
|
|
10699
|
+
"import",
|
|
10700
|
+
"refresh"
|
|
10701
|
+
];
|
|
10702
|
+
var IGNORED_SUBCOMMANDS = ["output", "state", "fmt", "validate", "version", "workspace", "show"];
|
|
10703
|
+
function stripComments2(source) {
|
|
10704
|
+
return source.split("\n").map((line) => line.replace(/(^|\s)#.*$/, "$1")).join("\n");
|
|
10705
|
+
}
|
|
10706
|
+
function findWorkflowFiles(repoRoot) {
|
|
10707
|
+
const found = [];
|
|
10708
|
+
const walk2 = (dir, relative8) => {
|
|
10709
|
+
let entries;
|
|
10710
|
+
try {
|
|
10711
|
+
entries = readdirSync17(dir);
|
|
10712
|
+
} catch {
|
|
10713
|
+
return;
|
|
10714
|
+
}
|
|
10715
|
+
for (const entry of entries) {
|
|
10716
|
+
if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
|
|
10717
|
+
const full = join40(dir, entry);
|
|
10718
|
+
const rel = relative8 ? `${relative8}/${entry}` : entry;
|
|
10719
|
+
if (statSync10(full).isDirectory()) {
|
|
10720
|
+
walk2(full, rel);
|
|
10721
|
+
} else if (/\.ya?ml$/.test(entry) && relative8.endsWith(".github/workflows")) {
|
|
10722
|
+
found.push(rel);
|
|
10723
|
+
}
|
|
10724
|
+
}
|
|
10725
|
+
};
|
|
10726
|
+
walk2(repoRoot, "");
|
|
10727
|
+
return found.sort();
|
|
10728
|
+
}
|
|
10729
|
+
function checkWorkflowSource(file, rawSource) {
|
|
10730
|
+
const violations = [];
|
|
10731
|
+
const source = stripComments2(rawSource);
|
|
10732
|
+
const lines = source.split("\n");
|
|
10733
|
+
let sawGuardedInvocation = false;
|
|
10734
|
+
lines.forEach((line, index) => {
|
|
10735
|
+
const match = /(?:^|[\s;&|(])terraform\s+([a-z-]+)/.exec(line);
|
|
10736
|
+
if (!match) return;
|
|
10737
|
+
const subcommand = match[1];
|
|
10738
|
+
if (subcommand === void 0) return;
|
|
10739
|
+
if (IGNORED_SUBCOMMANDS.includes(subcommand)) return;
|
|
10740
|
+
if (!GUARDED_SUBCOMMANDS.includes(subcommand)) return;
|
|
10741
|
+
sawGuardedInvocation = true;
|
|
10742
|
+
if (!/-input=false/.test(line)) {
|
|
10743
|
+
violations.push({
|
|
10744
|
+
file,
|
|
10745
|
+
line: index + 1,
|
|
10746
|
+
message: `terraform ${subcommand} is missing -input=false. Without it Terraform prompts on stdin for any unresolved variable and blocks forever in CI (issue #322). Note -auto-approve does not suppress variable prompts.`
|
|
10747
|
+
});
|
|
10748
|
+
}
|
|
10749
|
+
});
|
|
10750
|
+
if (sawGuardedInvocation && !/^\s*TF_INPUT\s*:/m.test(source)) {
|
|
10751
|
+
violations.push({
|
|
10752
|
+
file,
|
|
10753
|
+
line: 1,
|
|
10754
|
+
message: `workflow runs Terraform but does not set TF_INPUT. Add "TF_INPUT: '0'" to the workflow-level env block as a backstop (issue #322).`
|
|
10755
|
+
});
|
|
10756
|
+
}
|
|
10757
|
+
return violations;
|
|
10758
|
+
}
|
|
10759
|
+
function checkTerraformInput(repoRoot) {
|
|
10760
|
+
return findWorkflowFiles(repoRoot).flatMap(
|
|
10761
|
+
(file) => checkWorkflowSource(file, readFileSync30(join40(repoRoot, file), "utf8"))
|
|
10762
|
+
);
|
|
10763
|
+
}
|
|
10764
|
+
|
|
10765
|
+
// src/lib/lambda-output-guard.ts
|
|
10766
|
+
var GUARDED_LAMBDA_SUBCOMMANDS = [
|
|
10767
|
+
"update-function-code",
|
|
10768
|
+
"update-function-configuration"
|
|
10769
|
+
];
|
|
10770
|
+
function joinContinuations(source) {
|
|
10771
|
+
const rawLines = source.split("\n");
|
|
10772
|
+
const logical = [];
|
|
10773
|
+
let buffer = "";
|
|
10774
|
+
let start = 0;
|
|
10775
|
+
rawLines.forEach((raw, index) => {
|
|
10776
|
+
const lineNo = index + 1;
|
|
10777
|
+
const trimmedEnd = raw.replace(/\s+$/, "");
|
|
10778
|
+
const continues = trimmedEnd.endsWith("\\");
|
|
10779
|
+
const piece = continues ? trimmedEnd.slice(0, -1) : trimmedEnd;
|
|
10780
|
+
if (buffer === "") start = lineNo;
|
|
10781
|
+
buffer = buffer === "" ? piece : `${buffer} ${piece.replace(/^\s+/, "")}`;
|
|
10782
|
+
if (!continues) {
|
|
10783
|
+
logical.push({ text: buffer, startLine: start });
|
|
10784
|
+
buffer = "";
|
|
10785
|
+
}
|
|
10786
|
+
});
|
|
10787
|
+
if (buffer !== "") logical.push({ text: buffer, startLine: start });
|
|
10788
|
+
return logical;
|
|
10789
|
+
}
|
|
10790
|
+
function isOutputSuppressed(command) {
|
|
10791
|
+
if (/>\s*\/dev\/null/.test(command)) return true;
|
|
10792
|
+
const hasTextOutput = /--output\s+text\b/.test(command);
|
|
10793
|
+
const queryMatch = /--query\s+(['"]?)([^\s'"]+)\1/.exec(command);
|
|
10794
|
+
if (hasTextOutput && queryMatch) {
|
|
10795
|
+
const projection = queryMatch[2] ?? "";
|
|
10796
|
+
if (!/environment/i.test(projection)) return true;
|
|
10797
|
+
}
|
|
10798
|
+
return false;
|
|
10799
|
+
}
|
|
10800
|
+
function checkWorkflowSource2(file, rawSource) {
|
|
10801
|
+
const violations = [];
|
|
10802
|
+
const source = stripComments2(rawSource);
|
|
10803
|
+
for (const { text, startLine } of joinContinuations(source)) {
|
|
10804
|
+
for (const subcommand of GUARDED_LAMBDA_SUBCOMMANDS) {
|
|
10805
|
+
const invocation = new RegExp(`(?:^|[\\s;&|(])aws\\s+lambda\\s+${subcommand}\\b`);
|
|
10806
|
+
if (!invocation.test(text)) continue;
|
|
10807
|
+
if (isOutputSuppressed(text)) continue;
|
|
10808
|
+
violations.push({
|
|
10809
|
+
file,
|
|
10810
|
+
line: startLine,
|
|
10811
|
+
message: `aws lambda ${subcommand} does not suppress its output. Its default is the full function configuration as JSON \u2014 Environment.Variables in plaintext \u2014 which leaks DB credentials into the Actions log (#334). Add --output text --query 'LastUpdateStatus' (or redirect to /dev/null).`
|
|
10812
|
+
});
|
|
10813
|
+
}
|
|
10814
|
+
}
|
|
10815
|
+
return violations;
|
|
10816
|
+
}
|
|
10817
|
+
function checkLambdaOutput(repoRoot) {
|
|
10818
|
+
return findWorkflowFiles(repoRoot).flatMap(
|
|
10819
|
+
(file) => checkWorkflowSource2(file, readFileSync31(join41(repoRoot, file), "utf8"))
|
|
10820
|
+
);
|
|
10821
|
+
}
|
|
10822
|
+
|
|
10823
|
+
// src/scripts/check-lambda-output.ts
|
|
10824
|
+
async function runLambdaOutputCheck() {
|
|
10825
|
+
const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10826
|
+
const files = findWorkflowFiles(root);
|
|
10827
|
+
console.log(`audited ${files.length} workflow file(s) under ${root}`);
|
|
10828
|
+
if (files.length === 0) {
|
|
10829
|
+
console.error(
|
|
10830
|
+
"\u2717 Lambda output guard: found 0 workflow files \u2014 this looks like a broken scan, not a clean repo. Refusing to report success over zero input."
|
|
10831
|
+
);
|
|
10832
|
+
process.exit(1);
|
|
10833
|
+
}
|
|
10834
|
+
const violations = checkLambdaOutput(root);
|
|
10835
|
+
if (violations.length > 0) {
|
|
10836
|
+
console.error("\u2717 Lambda output guard: unsuppressed aws lambda update-function-* output found\n");
|
|
10837
|
+
for (const v of violations) {
|
|
10838
|
+
console.error(` ${v.file}:${v.line} ${v.message}`);
|
|
10839
|
+
}
|
|
10840
|
+
console.error("\nSee biffo-template#334.");
|
|
10841
|
+
process.exit(1);
|
|
10842
|
+
}
|
|
10843
|
+
console.log(`\u2713 Lambda output guard: every aws lambda update-function-* call suppresses output`);
|
|
10844
|
+
}
|
|
10845
|
+
|
|
10846
|
+
// src/scripts/check-pipe-trap.ts
|
|
10847
|
+
import { readFileSync as readFileSync32, readdirSync as readdirSync18 } from "fs";
|
|
10848
|
+
import { join as join42, relative as relative6 } from "path";
|
|
10849
|
+
import { execa as execa12 } from "execa";
|
|
10850
|
+
|
|
10851
|
+
// src/lib/pipe-trap-guard.ts
|
|
10852
|
+
var STATUS_BEARING = [
|
|
10853
|
+
// Three-valued: 0 green, 1 failed, 2 cannot tell — and 2 is never a pass.
|
|
10854
|
+
/\bwait-for-checks\b/,
|
|
10855
|
+
/\bbranch-health\b/,
|
|
10856
|
+
/\bclaim\.sh\b/,
|
|
10857
|
+
/\bbiffo\.sh\s+(claim|wait-for-checks|branch-health|verify)\b/,
|
|
10858
|
+
// Push rejection is how commits get silently lost (AGENTS.md §4).
|
|
10859
|
+
/\bgit\s+push\b/,
|
|
10860
|
+
/\bverify\.sh\b/
|
|
10861
|
+
];
|
|
10862
|
+
var QUERY_FLAGS = [/--list\b/, /--help\b/, /--version\b/];
|
|
10863
|
+
function strip(source) {
|
|
10864
|
+
return source.split("\n").map((raw) => {
|
|
10865
|
+
let out = "";
|
|
10866
|
+
let quote = null;
|
|
10867
|
+
for (let i = 0; i < raw.length; i++) {
|
|
10868
|
+
const c = raw[i];
|
|
10869
|
+
if (quote) {
|
|
10870
|
+
if (c === "\\" && quote === '"') {
|
|
10871
|
+
i++;
|
|
10872
|
+
continue;
|
|
10873
|
+
}
|
|
10874
|
+
if (c === quote) {
|
|
10875
|
+
quote = null;
|
|
10876
|
+
continue;
|
|
10877
|
+
}
|
|
10878
|
+
if (c === "$" && quote === '"') {
|
|
10879
|
+
out += c + (raw[i + 1] ?? "");
|
|
10880
|
+
i++;
|
|
10881
|
+
}
|
|
10882
|
+
continue;
|
|
10883
|
+
}
|
|
10884
|
+
if (c === '"' || c === "'") {
|
|
10885
|
+
quote = c;
|
|
10886
|
+
continue;
|
|
10887
|
+
}
|
|
10888
|
+
if (c === "\\") {
|
|
10889
|
+
i++;
|
|
10890
|
+
continue;
|
|
10891
|
+
}
|
|
10892
|
+
if (c === "#" && (out === "" || /\s/.test(out[out.length - 1] ?? ""))) break;
|
|
10893
|
+
out += c;
|
|
10894
|
+
}
|
|
10895
|
+
return out;
|
|
10896
|
+
});
|
|
10897
|
+
}
|
|
10898
|
+
function isPipeline(line) {
|
|
10899
|
+
for (let i = 0; i < line.length; i++) {
|
|
10900
|
+
if (line[i] !== "|") continue;
|
|
10901
|
+
if (line[i + 1] === "|") {
|
|
10902
|
+
i++;
|
|
10903
|
+
continue;
|
|
10904
|
+
}
|
|
10905
|
+
if (line[i - 1] === "|") continue;
|
|
10906
|
+
return true;
|
|
10907
|
+
}
|
|
10908
|
+
return false;
|
|
10909
|
+
}
|
|
10910
|
+
function findPipeTraps(source) {
|
|
10911
|
+
const lines = strip(source);
|
|
10912
|
+
const traps = [];
|
|
10913
|
+
let lastWasPipeline = false;
|
|
10914
|
+
lines.forEach((line, index) => {
|
|
10915
|
+
const trimmed = line.trim();
|
|
10916
|
+
if (trimmed === "") return;
|
|
10917
|
+
if (isPipeline(line)) {
|
|
10918
|
+
const head = line.split("|")[0] ?? "";
|
|
10919
|
+
const matched = STATUS_BEARING.find((p) => p.test(head));
|
|
10920
|
+
const isQuery = QUERY_FLAGS.some((p) => p.test(head));
|
|
10921
|
+
if (matched && !isQuery) {
|
|
10922
|
+
traps.push({
|
|
10923
|
+
line: index + 1,
|
|
10924
|
+
text: trimmed,
|
|
10925
|
+
reason: `a status-bearing command upstream of a pipe \u2014 the pipeline reports the LAST command's status, so this can only ever read 0`
|
|
10926
|
+
});
|
|
10927
|
+
}
|
|
10928
|
+
}
|
|
10929
|
+
if (lastWasPipeline && /\$\?/.test(line)) {
|
|
10930
|
+
traps.push({
|
|
10931
|
+
line: index + 1,
|
|
10932
|
+
text: trimmed,
|
|
10933
|
+
reason: `$? read directly after a pipeline \u2014 that is the LAST command's status, not the one you meant. Use \${PIPESTATUS[0]}, or drop the pipe`
|
|
10934
|
+
});
|
|
10935
|
+
}
|
|
10936
|
+
lastWasPipeline = isPipeline(line);
|
|
10937
|
+
});
|
|
10938
|
+
return traps;
|
|
10939
|
+
}
|
|
10940
|
+
|
|
10941
|
+
// src/scripts/check-pipe-trap.ts
|
|
10942
|
+
function shellFiles(root) {
|
|
10943
|
+
const out = [];
|
|
10944
|
+
for (const dir of ["scripts", ".githooks"]) {
|
|
10945
|
+
const full = join42(root, dir);
|
|
10946
|
+
let entries;
|
|
10947
|
+
try {
|
|
10948
|
+
entries = readdirSync18(full, { withFileTypes: true });
|
|
10949
|
+
} catch {
|
|
10950
|
+
continue;
|
|
10951
|
+
}
|
|
10952
|
+
for (const entry of entries) {
|
|
10953
|
+
if (!entry.isFile()) continue;
|
|
10954
|
+
if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
|
|
10955
|
+
out.push(join42(full, entry.name));
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
return out;
|
|
10959
|
+
}
|
|
10960
|
+
async function runPipeTrapCheck() {
|
|
10961
|
+
const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10962
|
+
const files = shellFiles(root);
|
|
10963
|
+
console.log(`audited ${files.length} shell file(s) under scripts/ and .githooks/ under ${root}`);
|
|
10964
|
+
if (files.length === 0) {
|
|
10965
|
+
console.error(
|
|
10966
|
+
"\u2717 Pipe-trap guard: found 0 shell files under scripts/ or .githooks/ \u2014 this looks like a broken scan, not a clean repo. Refusing to report success over zero input."
|
|
10967
|
+
);
|
|
10968
|
+
process.exit(1);
|
|
10969
|
+
}
|
|
10970
|
+
const findings = files.flatMap(
|
|
10971
|
+
(file) => findPipeTraps(readFileSync32(file, "utf8")).map(
|
|
10972
|
+
(t) => `${relative6(root, file)}:${t.line} ${t.text}
|
|
10973
|
+
${t.reason}`
|
|
10974
|
+
)
|
|
10975
|
+
);
|
|
10976
|
+
if (findings.length > 0) {
|
|
10977
|
+
console.error("\u2717 Pipe-trap guard: status-bearing pipeline(s) found\n");
|
|
10978
|
+
for (const f of findings) {
|
|
10979
|
+
console.error(` ${f}`);
|
|
10980
|
+
}
|
|
10981
|
+
console.error("\nSee biffo-template#1231 (AGENTS.md \xA74).");
|
|
10982
|
+
process.exit(1);
|
|
10983
|
+
}
|
|
10984
|
+
console.log(`\u2713 Pipe-trap guard: no status-bearing command is piped away`);
|
|
10985
|
+
}
|
|
10986
|
+
|
|
10555
10987
|
// src/scripts/check-plugin-collisions.ts
|
|
10556
10988
|
import { existsSync as existsSync37 } from "fs";
|
|
10557
|
-
import { join as
|
|
10558
|
-
import { execa as
|
|
10989
|
+
import { join as join44 } from "path";
|
|
10990
|
+
import { execa as execa13 } from "execa";
|
|
10559
10991
|
|
|
10560
10992
|
// src/lib/plugin-collision-guard.ts
|
|
10561
|
-
import { existsSync as existsSync36, readdirSync as
|
|
10562
|
-
import { join as
|
|
10993
|
+
import { existsSync as existsSync36, readdirSync as readdirSync19, statSync as statSync11 } from "fs";
|
|
10994
|
+
import { join as join43 } from "path";
|
|
10563
10995
|
var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
|
|
10564
10996
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
|
|
10565
10997
|
function subdirectories(dir) {
|
|
10566
10998
|
if (!existsSync36(dir)) return [];
|
|
10567
|
-
return
|
|
10999
|
+
return readdirSync19(dir).filter((entry) => {
|
|
10568
11000
|
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
|
|
10569
11001
|
try {
|
|
10570
|
-
return
|
|
11002
|
+
return statSync11(join43(dir, entry)).isDirectory();
|
|
10571
11003
|
} catch {
|
|
10572
11004
|
return false;
|
|
10573
11005
|
}
|
|
10574
11006
|
});
|
|
10575
11007
|
}
|
|
10576
11008
|
function regularPackagesOf(pluginDir2) {
|
|
10577
|
-
return subdirectories(pluginDir2).filter((name) => existsSync36(
|
|
11009
|
+
return subdirectories(pluginDir2).filter((name) => existsSync36(join43(pluginDir2, name, "__init__.py"))).sort();
|
|
10578
11010
|
}
|
|
10579
11011
|
function bareTestModulesOf(pluginDir2) {
|
|
10580
|
-
const testsDir =
|
|
11012
|
+
const testsDir = join43(pluginDir2, "tests");
|
|
10581
11013
|
if (!existsSync36(testsDir)) return [];
|
|
10582
|
-
if (existsSync36(
|
|
10583
|
-
return
|
|
11014
|
+
if (existsSync36(join43(testsDir, "__init__.py"))) return [];
|
|
11015
|
+
return readdirSync19(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
|
|
10584
11016
|
}
|
|
10585
11017
|
function findCollisions(servicesDir, pluginDirs) {
|
|
10586
11018
|
const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
|
|
@@ -10588,7 +11020,7 @@ function findCollisions(servicesDir, pluginDirs) {
|
|
|
10588
11020
|
const gather = (kind, namesOf) => {
|
|
10589
11021
|
const claims = /* @__PURE__ */ new Map();
|
|
10590
11022
|
for (const plugin of plugins) {
|
|
10591
|
-
for (const name of namesOf(
|
|
11023
|
+
for (const name of namesOf(join43(servicesDir, plugin))) {
|
|
10592
11024
|
claims.set(name, [...claims.get(name) ?? [], plugin]);
|
|
10593
11025
|
}
|
|
10594
11026
|
}
|
|
@@ -10625,8 +11057,8 @@ function formatCollisions(collisions) {
|
|
|
10625
11057
|
|
|
10626
11058
|
// src/scripts/check-plugin-collisions.ts
|
|
10627
11059
|
async function runPluginCollisionCheck() {
|
|
10628
|
-
const root = (await
|
|
10629
|
-
const servicesDir =
|
|
11060
|
+
const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
11061
|
+
const servicesDir = join44(root, "services");
|
|
10630
11062
|
if (!existsSync37(servicesDir)) {
|
|
10631
11063
|
console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
|
|
10632
11064
|
return;
|
|
@@ -10644,38 +11076,38 @@ async function runPluginCollisionCheck() {
|
|
|
10644
11076
|
}
|
|
10645
11077
|
|
|
10646
11078
|
// src/scripts/check-plugin-terraform.ts
|
|
10647
|
-
import { execa as
|
|
11079
|
+
import { execa as execa14 } from "execa";
|
|
10648
11080
|
|
|
10649
11081
|
// src/lib/plugin-terraform-guard.ts
|
|
10650
|
-
import { existsSync as existsSync38, readFileSync as
|
|
10651
|
-
import { dirname as dirname9, join as
|
|
11082
|
+
import { existsSync as existsSync38, readFileSync as readFileSync33, readdirSync as readdirSync20 } from "fs";
|
|
11083
|
+
import { dirname as dirname9, join as join45, relative as relative7, sep as sep3 } from "path";
|
|
10652
11084
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
|
|
10653
11085
|
var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
|
|
10654
11086
|
function findPluginManifests(root) {
|
|
10655
11087
|
const found = [];
|
|
10656
|
-
const
|
|
11088
|
+
const walk2 = (dir) => {
|
|
10657
11089
|
let entries;
|
|
10658
11090
|
try {
|
|
10659
|
-
entries =
|
|
11091
|
+
entries = readdirSync20(dir, { withFileTypes: true });
|
|
10660
11092
|
} catch {
|
|
10661
11093
|
return;
|
|
10662
11094
|
}
|
|
10663
11095
|
for (const entry of entries) {
|
|
10664
11096
|
if (entry.isDirectory()) {
|
|
10665
11097
|
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
10666
|
-
|
|
11098
|
+
walk2(join45(dir, entry.name));
|
|
10667
11099
|
} else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
|
|
10668
|
-
found.push(
|
|
11100
|
+
found.push(relative7(root, join45(dir, entry.name)).split(sep3).join("/"));
|
|
10669
11101
|
}
|
|
10670
11102
|
}
|
|
10671
11103
|
};
|
|
10672
|
-
|
|
11104
|
+
walk2(root);
|
|
10673
11105
|
return found.sort();
|
|
10674
11106
|
}
|
|
10675
11107
|
function readSubscriptions(absManifestPath) {
|
|
10676
11108
|
let parsed;
|
|
10677
11109
|
try {
|
|
10678
|
-
parsed = JSON.parse(
|
|
11110
|
+
parsed = JSON.parse(readFileSync33(absManifestPath, "utf8"));
|
|
10679
11111
|
} catch {
|
|
10680
11112
|
return null;
|
|
10681
11113
|
}
|
|
@@ -10690,15 +11122,15 @@ function readSubscriptions(absManifestPath) {
|
|
|
10690
11122
|
}
|
|
10691
11123
|
function checkPluginTerraform(root) {
|
|
10692
11124
|
const violations = [];
|
|
10693
|
-
const coreManifest = existsSync38(
|
|
11125
|
+
const coreManifest = existsSync38(join45(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
|
|
10694
11126
|
for (const manifest of findPluginManifests(root)) {
|
|
10695
11127
|
if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
|
|
10696
|
-
const absManifest =
|
|
11128
|
+
const absManifest = join45(root, manifest);
|
|
10697
11129
|
const subscriptions = readSubscriptions(absManifest);
|
|
10698
11130
|
if (subscriptions === null) continue;
|
|
10699
11131
|
const pluginDir2 = dirname9(absManifest);
|
|
10700
|
-
if (existsSync38(
|
|
10701
|
-
const relPluginDir =
|
|
11132
|
+
if (existsSync38(join45(pluginDir2, "terraform"))) continue;
|
|
11133
|
+
const relPluginDir = relative7(root, pluginDir2).split(sep3).join("/");
|
|
10702
11134
|
violations.push({
|
|
10703
11135
|
manifest,
|
|
10704
11136
|
expectedTerraformDir: relPluginDir ? `${relPluginDir}/terraform` : "terraform",
|
|
@@ -10717,7 +11149,7 @@ function formatViolations(violations) {
|
|
|
10717
11149
|
|
|
10718
11150
|
// src/scripts/check-plugin-terraform.ts
|
|
10719
11151
|
async function runPluginTerraformCheck() {
|
|
10720
|
-
const root = (await
|
|
11152
|
+
const root = (await execa14("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10721
11153
|
const violations = checkPluginTerraform(root);
|
|
10722
11154
|
if (violations.length > 0) {
|
|
10723
11155
|
console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
|
|
@@ -10729,12 +11161,12 @@ async function runPluginTerraformCheck() {
|
|
|
10729
11161
|
|
|
10730
11162
|
// src/scripts/check-plugin-tool-supply.ts
|
|
10731
11163
|
import { existsSync as existsSync40 } from "fs";
|
|
10732
|
-
import { join as
|
|
10733
|
-
import { execa as
|
|
11164
|
+
import { join as join47 } from "path";
|
|
11165
|
+
import { execa as execa15 } from "execa";
|
|
10734
11166
|
|
|
10735
11167
|
// src/lib/plugin-tool-supply-audit.ts
|
|
10736
|
-
import { existsSync as existsSync39, readFileSync as
|
|
10737
|
-
import { join as
|
|
11168
|
+
import { existsSync as existsSync39, readFileSync as readFileSync34, readdirSync as readdirSync21, statSync as statSync12 } from "fs";
|
|
11169
|
+
import { join as join46 } from "path";
|
|
10738
11170
|
|
|
10739
11171
|
// src/lib/openrouter-model-snapshot.ts
|
|
10740
11172
|
var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
|
|
@@ -11145,13 +11577,13 @@ var OPENROUTER_MODEL_IDS = [
|
|
|
11145
11577
|
function listDirs(root) {
|
|
11146
11578
|
let entries;
|
|
11147
11579
|
try {
|
|
11148
|
-
entries =
|
|
11580
|
+
entries = readdirSync21(root);
|
|
11149
11581
|
} catch {
|
|
11150
11582
|
return [];
|
|
11151
11583
|
}
|
|
11152
11584
|
return entries.filter((e) => {
|
|
11153
11585
|
try {
|
|
11154
|
-
return
|
|
11586
|
+
return statSync12(join46(root, e)).isDirectory();
|
|
11155
11587
|
} catch {
|
|
11156
11588
|
return false;
|
|
11157
11589
|
}
|
|
@@ -11159,30 +11591,30 @@ function listDirs(root) {
|
|
|
11159
11591
|
}
|
|
11160
11592
|
function walkFiles2(root, accept, skipDir) {
|
|
11161
11593
|
const out = [];
|
|
11162
|
-
const
|
|
11594
|
+
const walk2 = (dir) => {
|
|
11163
11595
|
let entries;
|
|
11164
11596
|
try {
|
|
11165
|
-
entries =
|
|
11597
|
+
entries = readdirSync21(dir);
|
|
11166
11598
|
} catch {
|
|
11167
11599
|
return;
|
|
11168
11600
|
}
|
|
11169
11601
|
for (const entry of entries) {
|
|
11170
|
-
const p =
|
|
11602
|
+
const p = join46(dir, entry);
|
|
11171
11603
|
let st;
|
|
11172
11604
|
try {
|
|
11173
|
-
st =
|
|
11605
|
+
st = statSync12(p);
|
|
11174
11606
|
} catch {
|
|
11175
11607
|
continue;
|
|
11176
11608
|
}
|
|
11177
11609
|
if (st.isDirectory()) {
|
|
11178
11610
|
if (skipDir(entry)) continue;
|
|
11179
|
-
|
|
11611
|
+
walk2(p);
|
|
11180
11612
|
continue;
|
|
11181
11613
|
}
|
|
11182
11614
|
if (accept(entry)) out.push(p);
|
|
11183
11615
|
}
|
|
11184
11616
|
};
|
|
11185
|
-
|
|
11617
|
+
walk2(root);
|
|
11186
11618
|
return out.sort();
|
|
11187
11619
|
}
|
|
11188
11620
|
function pluginPythonFiles(pluginDir2) {
|
|
@@ -11193,14 +11625,14 @@ function pluginPythonFiles(pluginDir2) {
|
|
|
11193
11625
|
);
|
|
11194
11626
|
}
|
|
11195
11627
|
function pluginTerraformFiles(pluginDir2) {
|
|
11196
|
-
const tfDir =
|
|
11628
|
+
const tfDir = join46(pluginDir2, "terraform");
|
|
11197
11629
|
let entries;
|
|
11198
11630
|
try {
|
|
11199
|
-
entries =
|
|
11631
|
+
entries = readdirSync21(tfDir);
|
|
11200
11632
|
} catch {
|
|
11201
11633
|
return [];
|
|
11202
11634
|
}
|
|
11203
|
-
return entries.filter((e) => e.endsWith(".tf")).map((e) =>
|
|
11635
|
+
return entries.filter((e) => e.endsWith(".tf")).map((e) => join46(tfDir, e)).sort();
|
|
11204
11636
|
}
|
|
11205
11637
|
function extractManifestTools(manifestText) {
|
|
11206
11638
|
let parsed;
|
|
@@ -11452,8 +11884,8 @@ function isSnapshotStale(fetchedAt, now) {
|
|
|
11452
11884
|
function normalizeModelId(id) {
|
|
11453
11885
|
return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
|
|
11454
11886
|
}
|
|
11455
|
-
var CONFIG_PY_PATH =
|
|
11456
|
-
var ORCHESTRATION_SCHEMA_PATH =
|
|
11887
|
+
var CONFIG_PY_PATH = join46("services", "api", "src", "api", "config.py");
|
|
11888
|
+
var ORCHESTRATION_SCHEMA_PATH = join46(
|
|
11457
11889
|
"services",
|
|
11458
11890
|
"api",
|
|
11459
11891
|
"src",
|
|
@@ -11465,8 +11897,8 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
11465
11897
|
const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
|
|
11466
11898
|
const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
|
|
11467
11899
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
11468
|
-
const configPath =
|
|
11469
|
-
const orchestrationPath =
|
|
11900
|
+
const configPath = join46(repoRoot, CONFIG_PY_PATH);
|
|
11901
|
+
const orchestrationPath = join46(repoRoot, ORCHESTRATION_SCHEMA_PATH);
|
|
11470
11902
|
const configMissing = !existsSync39(configPath);
|
|
11471
11903
|
const orchestrationSchemaMissing = !existsSync39(orchestrationPath);
|
|
11472
11904
|
const knownSet = new Set(knownModelIds);
|
|
@@ -11486,13 +11918,13 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
11486
11918
|
};
|
|
11487
11919
|
let settingsBlind = false;
|
|
11488
11920
|
if (!configMissing) {
|
|
11489
|
-
const settingsFields = extractSettingsModelFields(
|
|
11921
|
+
const settingsFields = extractSettingsModelFields(readFileSync34(configPath, "utf8"));
|
|
11490
11922
|
if (settingsFields.length === 0) settingsBlind = true;
|
|
11491
11923
|
for (const { field, value } of settingsFields) record(`${CONFIG_PY_PATH}#${field}`, value);
|
|
11492
11924
|
}
|
|
11493
11925
|
let curatedFieldsBlind = false;
|
|
11494
11926
|
if (!orchestrationSchemaMissing) {
|
|
11495
|
-
const curated = extractCuratedModelFields(
|
|
11927
|
+
const curated = extractCuratedModelFields(readFileSync34(orchestrationPath, "utf8"));
|
|
11496
11928
|
if (curated.rawFieldCount > 0 && curated.fields.every((f) => f.defaultValue === null && f.optionValues.length === 0)) {
|
|
11497
11929
|
curatedFieldsBlind = true;
|
|
11498
11930
|
}
|
|
@@ -11539,7 +11971,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
11539
11971
|
function discoverPluginDirs(pluginsRoot) {
|
|
11540
11972
|
return listDirs(pluginsRoot).filter((name) => {
|
|
11541
11973
|
try {
|
|
11542
|
-
return
|
|
11974
|
+
return statSync12(join46(pluginsRoot, name, "biffo.plugin.json")).isFile();
|
|
11543
11975
|
} catch {
|
|
11544
11976
|
return false;
|
|
11545
11977
|
}
|
|
@@ -11552,8 +11984,8 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
11552
11984
|
let terraformBlind = false;
|
|
11553
11985
|
let totalDeclaredTools = 0;
|
|
11554
11986
|
for (const name of pluginNames) {
|
|
11555
|
-
const pluginDir2 =
|
|
11556
|
-
const manifestText =
|
|
11987
|
+
const pluginDir2 = join46(pluginsRoot, name);
|
|
11988
|
+
const manifestText = readFileSync34(join46(pluginDir2, "biffo.plugin.json"), "utf8");
|
|
11557
11989
|
const manifest = extractManifestTools(manifestText);
|
|
11558
11990
|
if (manifest.parseError) {
|
|
11559
11991
|
findings.push({
|
|
@@ -11571,13 +12003,13 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
11571
12003
|
totalDeclaredTools += manifest.tools.length;
|
|
11572
12004
|
const pySources = pluginPythonFiles(pluginDir2).map((f) => ({
|
|
11573
12005
|
file: f,
|
|
11574
|
-
text:
|
|
12006
|
+
text: readFileSync34(f, "utf8")
|
|
11575
12007
|
}));
|
|
11576
12008
|
const resolver = buildSymbolResolver(pySources);
|
|
11577
12009
|
const registry = extractToolRegistryEntries(pySources, resolver);
|
|
11578
12010
|
if (registry.rawToolDefinitionCount > 0 && registry.entries.length === 0) registryBlind = true;
|
|
11579
12011
|
const tfFiles = pluginTerraformFiles(pluginDir2);
|
|
11580
|
-
const tfText = tfFiles.map((f) =>
|
|
12012
|
+
const tfText = tfFiles.map((f) => readFileSync34(f, "utf8")).join("\n");
|
|
11581
12013
|
const terraform = extractTerraformEnvKeys(tfText);
|
|
11582
12014
|
if (terraform.rawMarkerCount > 0 && terraform.resolvedBlockCount === 0) terraformBlind = true;
|
|
11583
12015
|
for (const toolName of manifest.tools) {
|
|
@@ -11651,7 +12083,7 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
11651
12083
|
requiredEnvVars: envResult.envVars,
|
|
11652
12084
|
missingEnvVars: anyWired ? [] : envResult.envVars,
|
|
11653
12085
|
status: anyWired ? "ok" : "missing-env",
|
|
11654
|
-
detail: anyWired ? `${entry.predicate}() is satisfiable: at least one of ${JSON.stringify(envResult.envVars)} is wired in Terraform` : `${entry.predicate}() reads ${JSON.stringify(envResult.envVars)} \u2014 NONE of these are wired by any environment_variables block under ${
|
|
12086
|
+
detail: anyWired ? `${entry.predicate}() is satisfiable: at least one of ${JSON.stringify(envResult.envVars)} is wired in Terraform` : `${entry.predicate}() reads ${JSON.stringify(envResult.envVars)} \u2014 NONE of these are wired by any environment_variables block under ${join46(pluginDir2, "terraform")}, so this deployment can never supply it`
|
|
11655
12087
|
});
|
|
11656
12088
|
}
|
|
11657
12089
|
}
|
|
@@ -11682,9 +12114,9 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
11682
12114
|
|
|
11683
12115
|
// src/scripts/check-plugin-tool-supply.ts
|
|
11684
12116
|
async function runPluginToolSupplyCheck() {
|
|
11685
|
-
const root = (await
|
|
12117
|
+
const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
11686
12118
|
let allOk = true;
|
|
11687
|
-
const pluginsRoot =
|
|
12119
|
+
const pluginsRoot = join47(root, "services", "_plugins");
|
|
11688
12120
|
if (!existsSync40(pluginsRoot)) {
|
|
11689
12121
|
console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
|
|
11690
12122
|
} else {
|
|
@@ -11715,7 +12147,7 @@ async function runPluginToolSupplyCheck() {
|
|
|
11715
12147
|
console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
|
|
11716
12148
|
}
|
|
11717
12149
|
}
|
|
11718
|
-
const servicesApiRoot =
|
|
12150
|
+
const servicesApiRoot = join47(root, "services", "api");
|
|
11719
12151
|
if (!existsSync40(servicesApiRoot)) {
|
|
11720
12152
|
console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
|
|
11721
12153
|
} else {
|
|
@@ -11762,7 +12194,7 @@ async function runPluginToolSupplyCheck() {
|
|
|
11762
12194
|
}
|
|
11763
12195
|
|
|
11764
12196
|
// src/scripts/check-release-subject.ts
|
|
11765
|
-
import { execa as
|
|
12197
|
+
import { execa as execa16 } from "execa";
|
|
11766
12198
|
|
|
11767
12199
|
// src/lib/release-version.ts
|
|
11768
12200
|
var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
|
|
@@ -11799,7 +12231,7 @@ async function fetchPrTitleViaGh({
|
|
|
11799
12231
|
PR_NUMBER,
|
|
11800
12232
|
GH_REPO
|
|
11801
12233
|
}) {
|
|
11802
|
-
const { stdout } = await
|
|
12234
|
+
const { stdout } = await execa16(
|
|
11803
12235
|
"gh",
|
|
11804
12236
|
["pr", "view", PR_NUMBER, "--repo", GH_REPO, "--json", "title", "--jq", ".title"],
|
|
11805
12237
|
{ env: { ...process.env, GH_TOKEN } }
|
|
@@ -11835,7 +12267,7 @@ async function resolveReleaseSubject({
|
|
|
11835
12267
|
);
|
|
11836
12268
|
}
|
|
11837
12269
|
}
|
|
11838
|
-
return (await
|
|
12270
|
+
return (await execa16("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
|
|
11839
12271
|
}
|
|
11840
12272
|
async function runReleaseSubjectCheck(argv) {
|
|
11841
12273
|
const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
|
|
@@ -11843,9 +12275,9 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
11843
12275
|
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
11844
12276
|
process.exit(2);
|
|
11845
12277
|
}
|
|
11846
|
-
const root = (await
|
|
11847
|
-
await
|
|
11848
|
-
const { stdout } = await
|
|
12278
|
+
const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12279
|
+
await execa16("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
12280
|
+
const { stdout } = await execa16("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
|
|
11849
12281
|
cwd: root
|
|
11850
12282
|
});
|
|
11851
12283
|
const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
@@ -11892,9 +12324,196 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
11892
12324
|
);
|
|
11893
12325
|
}
|
|
11894
12326
|
|
|
12327
|
+
// src/scripts/check-skeleton-drift.ts
|
|
12328
|
+
import { existsSync as existsSync41, readdirSync as readdirSync23 } from "fs";
|
|
12329
|
+
import { join as join49 } from "path";
|
|
12330
|
+
import { execa as execa17 } from "execa";
|
|
12331
|
+
|
|
12332
|
+
// src/lib/skeleton-drift-guard.ts
|
|
12333
|
+
import { readFileSync as readFileSync35, readdirSync as readdirSync22, statSync as statSync13 } from "fs";
|
|
12334
|
+
import { join as join48 } from "path";
|
|
12335
|
+
var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
|
|
12336
|
+
var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
|
|
12337
|
+
var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
|
|
12338
|
+
var executableLines = (contents) => contents.split("\n").filter((line) => !/^\s*#/.test(line));
|
|
12339
|
+
var UNHARDENED_AUDITS = [
|
|
12340
|
+
{ pattern: /\b(pnpm|npm|yarn)\s+audit\b/, replacement: "sh scripts/js-dependency-audit.sh" },
|
|
12341
|
+
{ pattern: /\bpip-audit\b/, replacement: "sh scripts/py-dependency-audit.sh" }
|
|
12342
|
+
];
|
|
12343
|
+
var SKELETON_RULES = [
|
|
12344
|
+
{
|
|
12345
|
+
id: "runner-label",
|
|
12346
|
+
rationale: "A hardcoded `ubuntu-latest` bills GitHub-hosted minutes and fails immediately on an account over its spending limit, which is the state this project is in \u2014 so a generated repo cannot reach the self-hosted fleet at all (#651).",
|
|
12347
|
+
appliesTo: isWorkflow,
|
|
12348
|
+
check: (_rel, contents) => {
|
|
12349
|
+
const hardcoded = [...contents.matchAll(/^\s*runs-on:\s*(.+)$/gm)].map((m) => m[1].trim()).filter((v) => !v.includes("RUNNER_LABEL"));
|
|
12350
|
+
return hardcoded.length > 0 ? `${hardcoded.length} job(s) pin a runner directly instead of \`\${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}\`: ${[...new Set(hardcoded)].join(", ")}` : null;
|
|
12351
|
+
}
|
|
12352
|
+
},
|
|
12353
|
+
{
|
|
12354
|
+
id: "no-gitleaks-action",
|
|
12355
|
+
rationale: "gitleaks/gitleaks-action@v2 cannot pass here for two independent reasons: its SARIF upload assumes a GitHub-hosted $HOME layout and dies on self-hosted runners, and it requires a paid licence for organization-owned repos. Every generated repo was born with a permanently red Secret Scan (#649).",
|
|
12356
|
+
appliesTo: isWorkflow,
|
|
12357
|
+
check: (_rel, contents) => /^\s*(-\s*)?uses:\s*gitleaks\/gitleaks-action/m.test(contents) ? "uses gitleaks/gitleaks-action; install the free CLI directly, as the root ci.yml does" : null
|
|
12358
|
+
},
|
|
12359
|
+
{
|
|
12360
|
+
id: "hardened-dependency-audit",
|
|
12361
|
+
rationale: "A raw `pnpm audit --audit-level=high` or `uv run pip-audit` exits non-zero identically whether it found a vulnerability or simply could not parse the registry's response, so one npm/PyPI hiccup reds a required check on every open PR at once \u2014 for an infrastructure blip with nothing to do with the code (#591). This repo hardened its own audits in #592/#636/#717/#721 and went on shipping the raw commands into every generated repo for months, so six siblings and two plugin repos were born with the original defect (#743). The wrappers parse the output first: they fail only on a genuine finding, retry a transient error, and report INCONCLUSIVE loudly rather than passing or failing blind.",
|
|
12362
|
+
appliesTo: isWorkflow,
|
|
12363
|
+
check: (_rel, contents) => {
|
|
12364
|
+
const offenders = executableLines(contents).filter(
|
|
12365
|
+
(line) => UNHARDENED_AUDITS.some((a) => a.pattern.test(line))
|
|
12366
|
+
);
|
|
12367
|
+
if (offenders.length === 0) return null;
|
|
12368
|
+
const wanted = [
|
|
12369
|
+
...new Set(
|
|
12370
|
+
UNHARDENED_AUDITS.filter((a) => offenders.some((line) => a.pattern.test(line))).map(
|
|
12371
|
+
(a) => a.replacement
|
|
12372
|
+
)
|
|
12373
|
+
)
|
|
12374
|
+
];
|
|
12375
|
+
return `${offenders.length} step(s) run an unhardened dependency audit directly: ${offenders.map((l) => l.trim()).join(" | ")}. Call ${wanted.join(" / ")} instead.`;
|
|
12376
|
+
}
|
|
12377
|
+
},
|
|
12378
|
+
{
|
|
12379
|
+
id: "derived-app-title",
|
|
12380
|
+
rationale: "A root layout that hard-codes `metadata.title` ships that literal as every generated repo's browser title, and nothing at scaffold time ever touches it. The sibling skeleton carried `title: 'Sibling App'` for its whole life, so two of tabsii's five siblings were still serving it in their deployed out/index.html \u2014 visible to users, not just in source (#963). Derive it from the build-time `NEXT_PUBLIC_SIBLING_NAME` instead (see the skeleton's own `src/lib/branding.ts`), the way the portal derives `PORTAL_TITLE` (#389).",
|
|
12381
|
+
appliesTo: isRootLayout,
|
|
12382
|
+
check: (_rel, contents) => {
|
|
12383
|
+
const match = /\btitle:\s*(['"`])([^'"`]*)\1/.exec(uncommented(contents));
|
|
12384
|
+
return match ? `metadata.title is the hard-coded literal ${match[1]}${match[2]}${match[1]}; derive it from the generated app\u2019s own name instead` : null;
|
|
12385
|
+
}
|
|
12386
|
+
}
|
|
12387
|
+
];
|
|
12388
|
+
function walk(dir, base = dir) {
|
|
12389
|
+
const out = [];
|
|
12390
|
+
let entries;
|
|
12391
|
+
try {
|
|
12392
|
+
entries = readdirSync22(dir);
|
|
12393
|
+
} catch {
|
|
12394
|
+
return out;
|
|
12395
|
+
}
|
|
12396
|
+
for (const entry of entries) {
|
|
12397
|
+
if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
|
|
12398
|
+
const abs = join48(dir, entry);
|
|
12399
|
+
let isDir;
|
|
12400
|
+
try {
|
|
12401
|
+
isDir = statSync13(abs).isDirectory();
|
|
12402
|
+
} catch {
|
|
12403
|
+
continue;
|
|
12404
|
+
}
|
|
12405
|
+
if (isDir) out.push(...walk(abs, base));
|
|
12406
|
+
else
|
|
12407
|
+
out.push(
|
|
12408
|
+
abs.slice(base.length + 1).split("\\").join("/")
|
|
12409
|
+
);
|
|
12410
|
+
}
|
|
12411
|
+
return out;
|
|
12412
|
+
}
|
|
12413
|
+
function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
|
|
12414
|
+
const violations = [];
|
|
12415
|
+
for (const rel of walk(skeletonRoot)) {
|
|
12416
|
+
for (const rule of rules) {
|
|
12417
|
+
if (!rule.appliesTo(rel)) continue;
|
|
12418
|
+
let contents;
|
|
12419
|
+
try {
|
|
12420
|
+
contents = readFileSync35(join48(skeletonRoot, rel), "utf8");
|
|
12421
|
+
} catch {
|
|
12422
|
+
continue;
|
|
12423
|
+
}
|
|
12424
|
+
const detail = rule.check(rel, contents);
|
|
12425
|
+
if (detail !== null) {
|
|
12426
|
+
violations.push({ skeleton: name, file: rel, rule: rule.id, detail });
|
|
12427
|
+
}
|
|
12428
|
+
}
|
|
12429
|
+
}
|
|
12430
|
+
return violations;
|
|
12431
|
+
}
|
|
12432
|
+
function formatViolations2(violations) {
|
|
12433
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
12434
|
+
for (const v of violations) {
|
|
12435
|
+
byRule.set(v.rule, [...byRule.get(v.rule) ?? [], v]);
|
|
12436
|
+
}
|
|
12437
|
+
const lines = [];
|
|
12438
|
+
for (const [ruleId, group] of byRule) {
|
|
12439
|
+
const rule = SKELETON_RULES.find((r) => r.id === ruleId);
|
|
12440
|
+
lines.push(` ${ruleId}:`);
|
|
12441
|
+
for (const v of group) lines.push(` ${v.skeleton}/${v.file} \u2014 ${v.detail}`);
|
|
12442
|
+
if (rule) lines.push(` why: ${rule.rationale}`);
|
|
12443
|
+
}
|
|
12444
|
+
return lines.join("\n");
|
|
12445
|
+
}
|
|
12446
|
+
|
|
12447
|
+
// src/scripts/check-skeleton-drift.ts
|
|
12448
|
+
function discoverSkeletons(root) {
|
|
12449
|
+
const skeletonsDir = join49(root, "_skeletons");
|
|
12450
|
+
let entries;
|
|
12451
|
+
try {
|
|
12452
|
+
entries = readdirSync23(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
12453
|
+
} catch {
|
|
12454
|
+
return [];
|
|
12455
|
+
}
|
|
12456
|
+
return entries.filter((name) => existsSync41(join49(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
|
|
12457
|
+
}
|
|
12458
|
+
async function runSkeletonDriftCheck() {
|
|
12459
|
+
const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12460
|
+
const skeletons = discoverSkeletons(root);
|
|
12461
|
+
let filesConsidered = 0;
|
|
12462
|
+
for (const name of skeletons) {
|
|
12463
|
+
const skeletonRoot = join49(root, "_skeletons", name);
|
|
12464
|
+
filesConsidered += findWorkflowFiles(skeletonRoot).length;
|
|
12465
|
+
if (existsSync41(join49(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
|
|
12466
|
+
filesConsidered += 1;
|
|
12467
|
+
}
|
|
12468
|
+
}
|
|
12469
|
+
console.log(
|
|
12470
|
+
`audited ${skeletons.length} skeleton(s) (${skeletons.join(", ") || "none"}), ${filesConsidered} file(s) considered, under ${root}/_skeletons`
|
|
12471
|
+
);
|
|
12472
|
+
if (skeletons.length === 0) {
|
|
12473
|
+
console.error(
|
|
12474
|
+
"\u2717 Skeleton-drift guard: found 0 repo skeletons under _skeletons/ \u2014 this looks like a broken scan, not a repo with no scaffolding. Refusing to report success over zero input."
|
|
12475
|
+
);
|
|
12476
|
+
process.exit(1);
|
|
12477
|
+
}
|
|
12478
|
+
const violations = skeletons.flatMap(
|
|
12479
|
+
(name) => auditSkeleton(join49(root, "_skeletons", name), name)
|
|
12480
|
+
);
|
|
12481
|
+
if (violations.length > 0) {
|
|
12482
|
+
console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
|
|
12483
|
+
console.error(formatViolations2(violations));
|
|
12484
|
+
console.error("\nSee skeleton-drift-guard.ts for why each rule exists.");
|
|
12485
|
+
process.exit(1);
|
|
12486
|
+
}
|
|
12487
|
+
console.log(`\u2713 Skeleton-drift guard: every skeleton holds every rule`);
|
|
12488
|
+
}
|
|
12489
|
+
|
|
12490
|
+
// src/scripts/check-terraform-input.ts
|
|
12491
|
+
import { execa as execa18 } from "execa";
|
|
12492
|
+
async function runTerraformInputCheck() {
|
|
12493
|
+
const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12494
|
+
const files = findWorkflowFiles(root);
|
|
12495
|
+
console.log(`audited ${files.length} workflow file(s) under ${root}`);
|
|
12496
|
+
if (files.length === 0) {
|
|
12497
|
+
console.error(
|
|
12498
|
+
"\u2717 Terraform-input guard: found 0 workflow files \u2014 this looks like a broken scan, not a clean repo. Refusing to report success over zero input."
|
|
12499
|
+
);
|
|
12500
|
+
process.exit(1);
|
|
12501
|
+
}
|
|
12502
|
+
const violations = checkTerraformInput(root);
|
|
12503
|
+
if (violations.length > 0) {
|
|
12504
|
+
console.error("\u2717 Terraform-input guard: interactive Terraform invocation(s) found\n");
|
|
12505
|
+
for (const v of violations) {
|
|
12506
|
+
console.error(` ${v.file}:${v.line} ${v.message}`);
|
|
12507
|
+
}
|
|
12508
|
+
console.error("\nSee biffo-template#322.");
|
|
12509
|
+
process.exit(1);
|
|
12510
|
+
}
|
|
12511
|
+
console.log(`\u2713 Terraform-input guard: every Terraform invocation is non-interactive`);
|
|
12512
|
+
}
|
|
12513
|
+
|
|
11895
12514
|
// src/commands/check.ts
|
|
11896
12515
|
var checkCommand = new Command23("check").description(
|
|
11897
|
-
"Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths) run in CI and git hooks, plus out-of-band audits (branch protection)"
|
|
12516
|
+
"Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, cognito-invite-template, lambda-output, pipe-trap, skeleton-drift, terraform-input) run in CI and git hooks, plus out-of-band audits (branch protection)"
|
|
11898
12517
|
);
|
|
11899
12518
|
checkCommand.command("ownership").description("Refuse changes to template-owned paths in an instance (#370)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").option("--staged <messageFile>", "Check staged changes instead of a branch diff (commit hook)").allowExcessArguments(true).action(async () => {
|
|
11900
12519
|
await runOwnershipCheck(rawArgsAfter("ownership"));
|
|
@@ -11928,6 +12547,31 @@ checkCommand.command("core-direct-paths").description(
|
|
|
11928
12547
|
).option("--sibling <name>", "Label for the report").option("--frontend-src <dir>", "Sibling's frontend source directory to scan").option("--core-src <dir>", "Core API's source directory (ground truth for route prefixes)").action(async (opts) => {
|
|
11929
12548
|
await runCoreDirectPathsCheck(opts);
|
|
11930
12549
|
});
|
|
12550
|
+
checkCommand.command("cognito-invite-template").description(
|
|
12551
|
+
"Refuse a Cognito invite_message_template missing a required member or placeholder (#356) \u2014 terraform validate is silent on this, so a fresh deploy fails on its first apply"
|
|
12552
|
+
).action(async () => {
|
|
12553
|
+
await runCognitoInviteTemplateCheck();
|
|
12554
|
+
});
|
|
12555
|
+
checkCommand.command("lambda-output").description(
|
|
12556
|
+
"Refuse an unsuppressed aws lambda update-function-* call (#334) \u2014 its default output is the full function configuration, env vars in plaintext, leaked into the Actions log"
|
|
12557
|
+
).action(async () => {
|
|
12558
|
+
await runLambdaOutputCheck();
|
|
12559
|
+
});
|
|
12560
|
+
checkCommand.command("pipe-trap").description(
|
|
12561
|
+
"Refuse a status-bearing command (claim.sh, wait-for-checks, git push, ...) piped into another, or $? read after one (#1231) \u2014 both read the LAST command's exit status"
|
|
12562
|
+
).action(async () => {
|
|
12563
|
+
await runPipeTrapCheck();
|
|
12564
|
+
});
|
|
12565
|
+
checkCommand.command("skeleton-drift").description(
|
|
12566
|
+
"Refuse a fix this repo made for itself that never reached _skeletons/ \u2014 a hardcoded runner, the paid gitleaks action, an unhardened dependency audit, a hard-coded app title"
|
|
12567
|
+
).action(async () => {
|
|
12568
|
+
await runSkeletonDriftCheck();
|
|
12569
|
+
});
|
|
12570
|
+
checkCommand.command("terraform-input").description(
|
|
12571
|
+
"Refuse a Terraform invocation that can prompt on stdin without -input=false, or a workflow running Terraform without TF_INPUT set (#322) \u2014 a runner has no stdin to answer"
|
|
12572
|
+
).action(async () => {
|
|
12573
|
+
await runTerraformInputCheck();
|
|
12574
|
+
});
|
|
11931
12575
|
checkCommand.command("branch-protection").description(
|
|
11932
12576
|
"Verify dev/staging/main are actually protected \u2014 scaffolding skips this on a 403 (#715)"
|
|
11933
12577
|
).option("--repo <owner/name>", "Repo to audit; defaults to this checkout's origin remote").option(
|
|
@@ -11942,8 +12586,8 @@ function rawArgsAfter(subcommand) {
|
|
|
11942
12586
|
}
|
|
11943
12587
|
|
|
11944
12588
|
// src/commands/doctor.ts
|
|
11945
|
-
import { existsSync as
|
|
11946
|
-
import { join as
|
|
12589
|
+
import { existsSync as existsSync42, readFileSync as readFileSync36 } from "fs";
|
|
12590
|
+
import { join as join50, resolve as resolve18 } from "path";
|
|
11947
12591
|
import chalk21 from "chalk";
|
|
11948
12592
|
import { Command as Command24 } from "commander";
|
|
11949
12593
|
|
|
@@ -12118,10 +12762,10 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
|
|
|
12118
12762
|
return runDoctorChecks(facts);
|
|
12119
12763
|
}
|
|
12120
12764
|
function readLocalCoreVersion(cwd) {
|
|
12121
|
-
const path =
|
|
12122
|
-
if (!
|
|
12765
|
+
const path = join50(cwd, INSTANCE_CORE_FILE);
|
|
12766
|
+
if (!existsSync42(path)) return null;
|
|
12123
12767
|
try {
|
|
12124
|
-
return parseCoreRecord(
|
|
12768
|
+
return parseCoreRecord(readFileSync36(path, "utf8"));
|
|
12125
12769
|
} catch {
|
|
12126
12770
|
return null;
|
|
12127
12771
|
}
|
|
@@ -12136,10 +12780,10 @@ function parseCoreRecord(contents) {
|
|
|
12136
12780
|
}
|
|
12137
12781
|
}
|
|
12138
12782
|
function readFossil(cwd) {
|
|
12139
|
-
const path =
|
|
12140
|
-
if (!
|
|
12783
|
+
const path = join50(cwd, CORE_VERSION_FILE);
|
|
12784
|
+
if (!existsSync42(path)) return null;
|
|
12141
12785
|
try {
|
|
12142
|
-
const value =
|
|
12786
|
+
const value = readFileSync36(path, "utf8").trim();
|
|
12143
12787
|
return value === "" ? null : value;
|
|
12144
12788
|
} catch {
|
|
12145
12789
|
return null;
|
|
@@ -12588,13 +13232,13 @@ import { fileURLToPath as fileURLToPath6 } from "url";
|
|
|
12588
13232
|
import { Command as Command26 } from "commander";
|
|
12589
13233
|
|
|
12590
13234
|
// src/lib/packaged-scripts.ts
|
|
12591
|
-
import { existsSync as
|
|
12592
|
-
import { dirname as dirname10, join as
|
|
13235
|
+
import { existsSync as existsSync43 } from "fs";
|
|
13236
|
+
import { dirname as dirname10, join as join51 } from "path";
|
|
12593
13237
|
function findPackagedScript(startDir, relativePath) {
|
|
12594
13238
|
let dir = startDir;
|
|
12595
13239
|
for (; ; ) {
|
|
12596
|
-
const candidate =
|
|
12597
|
-
if (
|
|
13240
|
+
const candidate = join51(dir, relativePath);
|
|
13241
|
+
if (existsSync43(candidate)) return candidate;
|
|
12598
13242
|
const parent = dirname10(dir);
|
|
12599
13243
|
if (parent === dir) return null;
|
|
12600
13244
|
dir = parent;
|