Skip to content

Commit 8a80813

Browse files
authored
chore(audit): execute 2026-06-05 (0914) audit — enforce no-as contract, dead-component guard, release-workflow least-privilege (#127)
* fix(ci): drop unused id-token:write from release workflows Add github-actions-permissions guardrail flagging id-token: write with no OIDC consumer (cosign/sigstore or cloud OIDC auth), comment-scrubbed so a 'reserved for future' note can't mask it. Remove the reserved-but-unused grant from both release workflows. Audit: F004 * fix(ui): make useFormField guards fire before context is consumed Both form contexts default to {} (truthy), so the old !fieldContext guard never fired and ran after fieldContext.name was already used. Guard on .name/.id before consuming them, and add the missing <FormItem> guard. Audit: F002 * fix(ui): validate API boundaries + enforce a real no-`as` ban repo-wide Boundary parsers no longer trust casts: openapi error bodies validate `message` and copy only well-typed fields; the SSE parser uses a type-guard (no `as`). The UI's `consistent-type-assertions` was set to assertionStyle "as" — which only enforces syntax, not a ban — so the merge-bar 'no `as`' contract was silently unenforced (the API uses "never"). Pinned the UI to "never", removed every production cast (logger mask, cursor pageParams via typed constants, Uint8Array web-push key, JSON.parse guards, and a sound Path<TForm> match in applyServerErrors), and relaxed only test files (documented, beside the existing no-unsafe-* test relaxations). Added a lint-meta guardrail `eslint-ban-type-assertions` to BOTH apps: it fails if a config weakens assertionStyle away from "never" or disables the rule without an audited eslint-meta-allow-assertion-exemption marker — the anti-drift rule that would have caught the original gap. Exhaustively unit-tested. Audit: F003 * fix(docs): add dead-component guard, delete 7 unused components apps/docs had no dead-code detection (knip's Astro/MDX entry graph is brittle to configure). Added a source-level check-unused-components.mjs — the equivalent of the knip step api/ui run — wired into build:ci. It flagged 7 unreferenced components (the audit's manual grep had found only 4): DocFileTree, FaqGroup, FaqItem, and docs-kit's CommandRun/FeatureGrid/PageIntro/SignalGrid. All deleted; build:ci green. Audit: F001
1 parent b08c25f commit 8a80813

51 files changed

Lines changed: 924 additions & 529 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/apps-api-release.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ concurrency:
2323
permissions:
2424
contents: read
2525
packages: write
26-
# Reserved for future cosign keyless signing; harmless when unused.
27-
id-token: write
2826

2927
jobs:
3028
build-and-push:

.github/workflows/apps-ui-release.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ permissions:
2828
contents: write
2929
pull-requests: write
3030
packages: write
31-
# Reserved for future cosign keyless signing.
32-
id-token: write
3331

3432
jobs:
3533
release:

apps/api/eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,7 @@ export default tseslint.config(
12411241
// cast is at the storage boundary.
12421242
files: ["src/lib/cache/providers/*.ts"],
12431243
rules: {
1244-
"@typescript-eslint/consistent-type-assertions": "off",
1244+
"@typescript-eslint/consistent-type-assertions": "off", // eslint-meta-allow-assertion-exemption: generic <T> cache storage boundary
12451245
},
12461246
},
12471247
{

apps/api/scripts/lint-meta/RULES.md

Lines changed: 37 additions & 36 deletions
Large diffs are not rendered by default.

apps/api/scripts/lint-meta/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { checkDependencyPairs } from "./rules/supply-chain/no-overlapping-libs";
2222
import { checkExactDependencyVersions } from "./rules/supply-chain/package-json-exact-deps";
2323
import { checkPackageOverrideParity } from "./rules/supply-chain/package-override-parity";
2424
import { checkSharedToolVersionParity } from "./rules/supply-chain/shared-tool-version-parity";
25+
import { checkEslintBanTypeAssertions } from "./rules/config/eslint-ban-type-assertions";
2526
import { checkEslintConfigNoWarn } from "./rules/config/eslint-config-no-warn";
2627
import { checkEslintOverridePathsExist } from "./rules/config/eslint-override-paths-exist";
2728
import { checkEslintPluginContractParity } from "./rules/config/eslint-plugin-contract-parity";
@@ -111,6 +112,7 @@ export {
111112
checkEnginePinParity,
112113
checkExactDependencyVersions,
113114
checkExternalClientTimeouts,
115+
checkEslintBanTypeAssertions,
114116
checkEslintConfigNoWarn,
115117
checkEslintOverridePathsExist,
116118
checkEslintPluginContractParity,

apps/api/scripts/lint-meta/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { githubActionsServiceImageDigestPinRule } from "./rules/ci/github-action
99
import { githubActionsTimeoutRequiredRule } from "./rules/ci/github-actions-timeout-required";
1010
import { prePushCiParityRule } from "./rules/ci/pre-push-ci-parity";
1111
import { tofuBootstrapHardeningRule } from "./rules/ci/tofu-bootstrap-hardening";
12+
import { eslintBanTypeAssertionsRule } from "./rules/config/eslint-ban-type-assertions";
1213
import { eslintConfigNoWarnRule } from "./rules/config/eslint-config-no-warn";
1314
import { eslintPluginContractParityRule } from "./rules/config/eslint-plugin-contract-parity";
1415
import { eslintOverridePathsExistRule } from "./rules/config/eslint-override-paths-exist";
@@ -60,6 +61,7 @@ export const META_RULES: readonly IMetaRule[] = [
6061
skippedTestsNeedTrackingRule,
6162
touchTestsTooRule,
6263
eslintConfigNoWarnRule,
64+
eslintBanTypeAssertionsRule,
6365
eslintOverridePathsExistRule,
6466
tsconfigIncludePathsExistRule,
6567
eslintPluginContractParityRule,

apps/api/scripts/lint-meta/rules/ci/github-actions-permissions.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,30 @@ import type { IMetaRule, IViolation } from "../../types";
44

55
const SHA_REGEX = /^[0-9a-f]{40}$/u;
66

7+
const ID_TOKEN_WRITE_REGEX = /^[ \t]*id-token:[ \t]*write\b/mu;
8+
9+
/*
10+
* `id-token: write` only does anything when a step exchanges the OIDC token —
11+
* keyless signing (cosign/sigstore) or cloud OIDC auth. Granting it with no
12+
* consumer hands every step in the job a needless token-minting capability.
13+
* This allowlist names the consumers we recognise; extend it when adding a new
14+
* OIDC integration so least-privilege stays enforced for template consumers.
15+
*/
16+
const OIDC_CONSUMER_REGEX =
17+
/cosign|sigstore|configure-aws-credentials|google-github-actions\/auth|azure\/login|vault-action|ACTIONS_ID_TOKEN_REQUEST/iu;
18+
19+
/*
20+
* Drop YAML comments so a "reserved for future cosign" note can't masquerade
21+
* as a real OIDC consumer.
22+
*/
23+
function stripYamlComments(text: string): string {
24+
return text.replace(/(^|[ \t])#.*$/gmu, "");
25+
}
26+
727
export function checkWorkflowShas(file: string): IViolation[] {
828
const violations: IViolation[] = [];
929
const text = readFileSync(file, "utf8");
30+
const scrubbed = stripYamlComments(text);
1031

1132
if (!/^permissions\s*:/mu.test(text)) {
1233
violations.push({
@@ -16,6 +37,18 @@ export function checkWorkflowShas(file: string): IViolation[] {
1637
});
1738
}
1839

40+
if (
41+
ID_TOKEN_WRITE_REGEX.test(scrubbed) &&
42+
!OIDC_CONSUMER_REGEX.test(scrubbed)
43+
) {
44+
violations.push({
45+
file,
46+
rule: "github-actions-permissions",
47+
message:
48+
"`id-token: write` is granted but no OIDC consumer (cosign/sigstore signing or cloud OIDC auth) uses it — drop the permission until a step needs it (least privilege).",
49+
});
50+
}
51+
1952
const usesRegex = /^\s*-?\s*uses:\s*([^\s#]+)/gmu;
2053
let match: RegExpExecArray | null = usesRegex.exec(text);
2154

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { existsSync, readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
4+
import type { IMetaRule, IViolation } from "../../types";
5+
6+
const RULE_ID = "eslint-ban-type-assertions";
7+
const CONFIG_NAMES = [
8+
"eslint.config.mjs",
9+
"eslint.config.js",
10+
"eslint.config.mts",
11+
"eslint.config.cjs",
12+
];
13+
const RULE_NAME = "@typescript-eslint/consistent-type-assertions";
14+
const EXEMPTION_MARKER = "eslint-meta-allow-assertion-exemption";
15+
16+
/*
17+
* `assertionStyle: "as" | "angle-bracket"` only enforces *which syntax* an
18+
* assertion uses — it still permits them. Only "never" bans `as` outright.
19+
* A config that drifts to "as" silently re-opens the hole (this is the exact
20+
* bug that once let casts ship: the UI rule was set to "as", not "never").
21+
*/
22+
const ASSERTION_STYLE_RE =
23+
/assertionStyle\s*:\s*["'](never|as|angle-bracket)["']/gu;
24+
const RULE_OFF_RE =
25+
/["']@typescript-eslint\/consistent-type-assertions["']\s*:\s*["']off["']/u;
26+
27+
// Strip a trailing line comment so a marker can't satisfy the style/off match.
28+
function stripLineComment(raw: string): string {
29+
return raw.replace(/\/\/.*$/u, "");
30+
}
31+
32+
function hasNeverPin(lines: readonly string[]): boolean {
33+
return lines.some((raw) =>
34+
[...stripLineComment(raw).matchAll(ASSERTION_STYLE_RE)].some(
35+
(match) => match[1] === "never"
36+
)
37+
);
38+
}
39+
40+
function lineViolations(
41+
file: string,
42+
raw: string,
43+
lineNo: number
44+
): IViolation[] {
45+
const out: IViolation[] = [];
46+
const code = stripLineComment(raw);
47+
48+
for (const match of code.matchAll(ASSERTION_STYLE_RE)) {
49+
if (match[1] !== "never") {
50+
out.push({
51+
file,
52+
rule: RULE_ID,
53+
message: `Line ${String(lineNo)}: \`assertionStyle: "${match[1] ?? ""}"\` still permits \`as\` casts — pin it to "never". A weaker style silently re-opens the type-assertion hole.`,
54+
});
55+
}
56+
}
57+
58+
// The marker must sit on the same line as the off (raw still has comments).
59+
if (RULE_OFF_RE.test(code) && !raw.includes(EXEMPTION_MARKER)) {
60+
out.push({
61+
file,
62+
rule: RULE_ID,
63+
message: `Line ${String(lineNo)}: \`${RULE_NAME}\` is disabled without justification — add \`// ${EXEMPTION_MARKER}: <reason>\` on the same line to sanction a genuine, audited type-boundary exemption.`,
64+
});
65+
}
66+
67+
return out;
68+
}
69+
70+
function checkConfigFile(file: string): IViolation[] {
71+
const lines = readFileSync(file, "utf8").split("\n");
72+
const violations = lines.flatMap((raw, i) =>
73+
lineViolations(file, raw, i + 1)
74+
);
75+
76+
if (!hasNeverPin(lines)) {
77+
violations.push({
78+
file,
79+
rule: RULE_ID,
80+
message: `${file}: \`${RULE_NAME}\` must be pinned to \`assertionStyle: "never"\`. The no-\`as\` rule is a core merge-bar contract and must be enforced explicitly, never assumed.`,
81+
});
82+
}
83+
84+
return violations;
85+
}
86+
87+
export function checkEslintBanTypeAssertions(root: string): IViolation[] {
88+
return CONFIG_NAMES.map((name) => join(root, name))
89+
.filter((full) => existsSync(full))
90+
.flatMap(checkConfigFile);
91+
}
92+
93+
/**
94+
* Guards the guard: the no-`as` merge-bar contract is only real while the
95+
* ESLint rule that enforces it stays pinned to `assertionStyle: "never"`. Fails
96+
* if a config weakens the style or disables the rule without an audited
97+
* `eslint-meta-allow-assertion-exemption` marker.
98+
*/
99+
export const eslintBanTypeAssertionsRule: IMetaRule = {
100+
id: RULE_ID,
101+
category: "config",
102+
description:
103+
'ESLint must pin @typescript-eslint/consistent-type-assertions to assertionStyle "never"; disabling it requires an audited eslint-meta-allow-assertion-exemption marker.',
104+
run({ root }) {
105+
return checkEslintBanTypeAssertions(root);
106+
},
107+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: unused-id-token
2+
on: push
3+
permissions:
4+
contents: read
5+
id-token: write
6+
jobs:
7+
build:
8+
runs-on: ubuntu-latest
9+
timeout-minutes: 5
10+
steps:
11+
- run: echo build
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: used-id-token
2+
on: push
3+
permissions:
4+
contents: read
5+
id-token: write
6+
jobs:
7+
sign:
8+
runs-on: ubuntu-latest
9+
timeout-minutes: 5
10+
steps:
11+
- run: cosign sign --yes "$IMAGE"

0 commit comments

Comments
 (0)