@intentius/chant-lexicon-github 0.1.23 → 0.3.0
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/integrity.json +34 -4
- package/dist/manifest.json +1 -1
- package/dist/rules/{gha020.ts → gha013.ts} +4 -4
- package/dist/rules/gha029.ts +68 -0
- package/dist/rules/gha030.ts +41 -0
- package/dist/rules/gha031.ts +77 -0
- package/dist/rules/gha032.ts +39 -0
- package/dist/rules/gha033.ts +47 -0
- package/dist/rules/gha034.ts +41 -0
- package/dist/rules/gha035.ts +66 -0
- package/dist/rules/gha036.ts +42 -0
- package/dist/rules/gha037.ts +46 -0
- package/dist/rules/gha038.ts +41 -0
- package/dist/rules/gha039.ts +41 -0
- package/dist/rules/gha040.ts +43 -0
- package/dist/rules/gha041.ts +38 -0
- package/dist/rules/gha042.ts +40 -0
- package/dist/rules/gha043.ts +41 -0
- package/dist/rules/gha044.ts +41 -0
- package/dist/rules/gha045.ts +38 -0
- package/dist/rules/gha046.ts +56 -0
- package/dist/rules/gha047.ts +51 -0
- package/dist/rules/gha048.ts +41 -0
- package/dist/rules/gha049.ts +42 -0
- package/dist/rules/gha050.ts +44 -0
- package/dist/rules/gha051.ts +45 -0
- package/dist/rules/gha052.ts +42 -0
- package/dist/rules/gha053.ts +41 -0
- package/dist/rules/gha054.ts +41 -0
- package/dist/rules/gha055.ts +50 -0
- package/dist/rules/gha056.ts +37 -0
- package/dist/rules/gha057.ts +40 -0
- package/dist/rules/gha058.ts +48 -0
- package/dist/rules/yaml-helpers.ts +335 -0
- package/package.json +1 -1
- package/src/codegen/docs.ts +273 -4
- package/src/composites/dependabot.test.ts +48 -0
- package/src/composites/dependabot.ts +91 -0
- package/src/composites/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/lint/post-synth/{gha020.test.ts → gha013.test.ts} +7 -7
- package/src/lint/post-synth/{gha020.ts → gha013.ts} +4 -4
- package/src/lint/post-synth/gha029.test.ts +108 -0
- package/src/lint/post-synth/gha029.ts +68 -0
- package/src/lint/post-synth/gha030.test.ts +87 -0
- package/src/lint/post-synth/gha030.ts +41 -0
- package/src/lint/post-synth/gha031.test.ts +75 -0
- package/src/lint/post-synth/gha031.ts +77 -0
- package/src/lint/post-synth/gha032.test.ts +65 -0
- package/src/lint/post-synth/gha032.ts +39 -0
- package/src/lint/post-synth/gha033.test.ts +68 -0
- package/src/lint/post-synth/gha033.ts +47 -0
- package/src/lint/post-synth/gha034.test.ts +86 -0
- package/src/lint/post-synth/gha034.ts +41 -0
- package/src/lint/post-synth/gha035.test.ts +85 -0
- package/src/lint/post-synth/gha035.ts +66 -0
- package/src/lint/post-synth/gha036.test.ts +68 -0
- package/src/lint/post-synth/gha036.ts +42 -0
- package/src/lint/post-synth/gha037.test.ts +66 -0
- package/src/lint/post-synth/gha037.ts +46 -0
- package/src/lint/post-synth/gha038.test.ts +64 -0
- package/src/lint/post-synth/gha038.ts +41 -0
- package/src/lint/post-synth/gha039.test.ts +66 -0
- package/src/lint/post-synth/gha039.ts +41 -0
- package/src/lint/post-synth/gha040.test.ts +77 -0
- package/src/lint/post-synth/gha040.ts +43 -0
- package/src/lint/post-synth/gha041.test.ts +48 -0
- package/src/lint/post-synth/gha041.ts +38 -0
- package/src/lint/post-synth/gha042.test.ts +53 -0
- package/src/lint/post-synth/gha042.ts +40 -0
- package/src/lint/post-synth/gha043.test.ts +69 -0
- package/src/lint/post-synth/gha043.ts +41 -0
- package/src/lint/post-synth/gha044.test.ts +56 -0
- package/src/lint/post-synth/gha044.ts +41 -0
- package/src/lint/post-synth/gha045.test.ts +51 -0
- package/src/lint/post-synth/gha045.ts +38 -0
- package/src/lint/post-synth/gha046.test.ts +59 -0
- package/src/lint/post-synth/gha046.ts +56 -0
- package/src/lint/post-synth/gha047.test.ts +66 -0
- package/src/lint/post-synth/gha047.ts +51 -0
- package/src/lint/post-synth/gha048.test.ts +51 -0
- package/src/lint/post-synth/gha048.ts +41 -0
- package/src/lint/post-synth/gha049.test.ts +72 -0
- package/src/lint/post-synth/gha049.ts +42 -0
- package/src/lint/post-synth/gha050.test.ts +55 -0
- package/src/lint/post-synth/gha050.ts +44 -0
- package/src/lint/post-synth/gha051.test.ts +72 -0
- package/src/lint/post-synth/gha051.ts +45 -0
- package/src/lint/post-synth/gha052.test.ts +52 -0
- package/src/lint/post-synth/gha052.ts +42 -0
- package/src/lint/post-synth/gha053.test.ts +65 -0
- package/src/lint/post-synth/gha053.ts +41 -0
- package/src/lint/post-synth/gha054.test.ts +49 -0
- package/src/lint/post-synth/gha054.ts +41 -0
- package/src/lint/post-synth/gha055.test.ts +49 -0
- package/src/lint/post-synth/gha055.ts +50 -0
- package/src/lint/post-synth/gha056.test.ts +47 -0
- package/src/lint/post-synth/gha056.ts +37 -0
- package/src/lint/post-synth/gha057.test.ts +61 -0
- package/src/lint/post-synth/gha057.ts +40 -0
- package/src/lint/post-synth/gha058.test.ts +67 -0
- package/src/lint/post-synth/gha058.ts +48 -0
- package/src/lint/post-synth/yaml-helpers.ts +335 -0
- package/src/lint/rules/data/flagged-actions.ts +36 -0
- package/src/lint/rules/data/known-action-slugs.ts +38 -0
- package/src/lint/rules/data/preinstalled-tools.ts +36 -0
- package/src/lint/rules/data/risky-features.ts +39 -0
- package/src/lint/rules/data/trusted-action-owners.ts +12 -0
- package/src/lint/rules/data/untrusted-contexts.ts +44 -0
- package/src/mcp/context-tools.test.ts +108 -0
- package/src/mcp/context-tools.ts +172 -0
- package/src/plugin.test.ts +58 -3
- package/src/plugin.ts +5 -1
- package/src/serializer.ts +38 -5
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha053 } from "./gha053";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA053: unsafe workflow-command opt-in", () => {
|
|
20
|
+
test("flags ACTIONS_ALLOW_UNSECURE_COMMANDS", () => {
|
|
21
|
+
const yaml = `name: CI
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
env:
|
|
28
|
+
ACTIONS_ALLOW_UNSECURE_COMMANDS: true
|
|
29
|
+
steps:
|
|
30
|
+
- run: echo build
|
|
31
|
+
`;
|
|
32
|
+
const diags = gha053.check(makeCtx(yaml));
|
|
33
|
+
expect(diags).toHaveLength(1);
|
|
34
|
+
expect(diags[0].checkId).toBe("GHA053");
|
|
35
|
+
expect(diags[0].severity).toBe("error");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("flags ::set-env:: emission", () => {
|
|
39
|
+
const yaml = `name: CI
|
|
40
|
+
on:
|
|
41
|
+
push:
|
|
42
|
+
jobs:
|
|
43
|
+
build:
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- run: echo "::set-env name=FOO::bar"
|
|
47
|
+
`;
|
|
48
|
+
const diags = gha053.check(makeCtx(yaml));
|
|
49
|
+
expect(diags).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("does not flag normal env file usage", () => {
|
|
53
|
+
const yaml = `name: CI
|
|
54
|
+
on:
|
|
55
|
+
push:
|
|
56
|
+
jobs:
|
|
57
|
+
build:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- run: echo "FOO=bar" >> "$GITHUB_ENV"
|
|
61
|
+
`;
|
|
62
|
+
const diags = gha053.check(makeCtx(yaml));
|
|
63
|
+
expect(diags).toHaveLength(0);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA053: Re-enabling Unsafe Workflow-Command Processing
|
|
3
|
+
*
|
|
4
|
+
* Flags opt-ins that reintroduce the `set-env` / `add-path` workflow commands
|
|
5
|
+
* removed for security (CVE-2020-15228) — `ACTIONS_ALLOW_UNSECURE_COMMANDS` and
|
|
6
|
+
* direct `::set-env::` / `::add-path::` emission. These let a logged string set
|
|
7
|
+
* arbitrary environment/PATH state for later steps, a code-execution path.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { getPrimaryOutput, linesByJob } from "./yaml-helpers";
|
|
12
|
+
|
|
13
|
+
const UNSAFE_COMMANDS = /ACTIONS_ALLOW_UNSECURE_COMMANDS|::set-env\s|::add-path\s/;
|
|
14
|
+
|
|
15
|
+
export const gha053: PostSynthCheck = {
|
|
16
|
+
id: "GHA053",
|
|
17
|
+
description: "Re-enables unsafe set-env / add-path workflow commands",
|
|
18
|
+
|
|
19
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
20
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
21
|
+
|
|
22
|
+
for (const [, output] of ctx.outputs) {
|
|
23
|
+
const yaml = getPrimaryOutput(output);
|
|
24
|
+
const seen = new Set<string>();
|
|
25
|
+
for (const [job, lines] of linesByJob(yaml)) {
|
|
26
|
+
if (lines.some((l) => UNSAFE_COMMANDS.test(l)) && !seen.has(job)) {
|
|
27
|
+
seen.add(job);
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
checkId: "GHA053",
|
|
30
|
+
severity: "error",
|
|
31
|
+
message: `Job "${job}" re-enables the unsafe set-env/add-path workflow commands (ACTIONS_ALLOW_UNSECURE_COMMANDS or ::set-env::/::add-path::). Use $GITHUB_ENV / $GITHUB_PATH files instead — the legacy commands were removed for security.`,
|
|
32
|
+
entity: job,
|
|
33
|
+
lexicon: "github",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return diagnostics;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha054 } from "./gha054";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA054: known-bad feature usage", () => {
|
|
20
|
+
test("flags deprecated ::set-output::", () => {
|
|
21
|
+
const yaml = `name: CI
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- run: echo "::set-output name=version::1.0.0"
|
|
29
|
+
`;
|
|
30
|
+
const diags = gha054.check(makeCtx(yaml));
|
|
31
|
+
expect(diags).toHaveLength(1);
|
|
32
|
+
expect(diags[0].checkId).toBe("GHA054");
|
|
33
|
+
expect(diags[0].message).toContain("set-output");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("does not flag $GITHUB_OUTPUT usage", () => {
|
|
37
|
+
const yaml = `name: CI
|
|
38
|
+
on:
|
|
39
|
+
push:
|
|
40
|
+
jobs:
|
|
41
|
+
build:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- run: echo "version=1.0.0" >> "$GITHUB_OUTPUT"
|
|
45
|
+
`;
|
|
46
|
+
const diags = gha054.check(makeCtx(yaml));
|
|
47
|
+
expect(diags).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA054: Use of a Feature with a Known Security Footgun
|
|
3
|
+
*
|
|
4
|
+
* Catch-all, data-driven check: flags emitted workflow content matching a
|
|
5
|
+
* vendored snapshot of known-risky GitHub Actions features (deprecated workflow
|
|
6
|
+
* commands, unsafe runtime opt-ins, masking footguns). Advisory; the dataset is
|
|
7
|
+
* necessarily incomplete.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { getPrimaryOutput, linesByJob } from "./yaml-helpers";
|
|
12
|
+
import { RISKY_FEATURES } from "../rules/data/risky-features";
|
|
13
|
+
|
|
14
|
+
export const gha054: PostSynthCheck = {
|
|
15
|
+
id: "GHA054",
|
|
16
|
+
description: "Use of a feature with a known security footgun",
|
|
17
|
+
|
|
18
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
19
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [, output] of ctx.outputs) {
|
|
22
|
+
const yaml = getPrimaryOutput(output);
|
|
23
|
+
for (const [job, lines] of linesByJob(yaml)) {
|
|
24
|
+
const text = lines.join("\n");
|
|
25
|
+
for (const feature of RISKY_FEATURES) {
|
|
26
|
+
if (feature.pattern.test(text)) {
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
checkId: "GHA054",
|
|
29
|
+
severity: "warning",
|
|
30
|
+
message: `Job "${job}" uses ${feature.label} — ${feature.advice}.`,
|
|
31
|
+
entity: job,
|
|
32
|
+
lexicon: "github",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return diagnostics;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha055 } from "./gha055";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA055: redundant runtime install", () => {
|
|
20
|
+
test("flags apt-get install of a preinstalled tool", () => {
|
|
21
|
+
const yaml = `name: CI
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
jobs:
|
|
25
|
+
build:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
steps:
|
|
28
|
+
- run: sudo apt-get install -y jq
|
|
29
|
+
`;
|
|
30
|
+
const diags = gha055.check(makeCtx(yaml));
|
|
31
|
+
expect(diags).toHaveLength(1);
|
|
32
|
+
expect(diags[0].checkId).toBe("GHA055");
|
|
33
|
+
expect(diags[0].message).toContain("jq");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("does not flag installing a tool not on the runner", () => {
|
|
37
|
+
const yaml = `name: CI
|
|
38
|
+
on:
|
|
39
|
+
push:
|
|
40
|
+
jobs:
|
|
41
|
+
build:
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
steps:
|
|
44
|
+
- run: sudo apt-get install -y libpq-dev
|
|
45
|
+
`;
|
|
46
|
+
const diags = gha055.check(makeCtx(yaml));
|
|
47
|
+
expect(diags).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA055: Runtime Install of a Tool Already on the Runner
|
|
3
|
+
*
|
|
4
|
+
* Flags a `run:` step that installs (apt/brew/apk/choco) a tool that GitHub-
|
|
5
|
+
* hosted runners already ship. The redundant install adds a third-party
|
|
6
|
+
* download and supply-chain surface for no benefit and slows the job. Advisory;
|
|
7
|
+
* irrelevant on self-hosted runners that may lack the tool.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { getPrimaryOutput, extractRunBlocks } from "./yaml-helpers";
|
|
12
|
+
import { PREINSTALLED_TOOLS } from "../rules/data/preinstalled-tools";
|
|
13
|
+
|
|
14
|
+
const INSTALL_RE = /\b(?:apt(?:-get)?\s+install|brew\s+install|apk\s+add|choco\s+install)\b([^\n]*)/g;
|
|
15
|
+
|
|
16
|
+
export const gha055: PostSynthCheck = {
|
|
17
|
+
id: "GHA055",
|
|
18
|
+
description: "Runtime install of a tool already present on the runner",
|
|
19
|
+
|
|
20
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
21
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
22
|
+
const preinstalled = new Set(PREINSTALLED_TOOLS);
|
|
23
|
+
|
|
24
|
+
for (const [, output] of ctx.outputs) {
|
|
25
|
+
const yaml = getPrimaryOutput(output);
|
|
26
|
+
for (const { job, run } of extractRunBlocks(yaml)) {
|
|
27
|
+
const redundant = new Set<string>();
|
|
28
|
+
let m: RegExpExecArray | null;
|
|
29
|
+
INSTALL_RE.lastIndex = 0;
|
|
30
|
+
while ((m = INSTALL_RE.exec(run)) !== null) {
|
|
31
|
+
for (const tok of m[1].split(/\s+/)) {
|
|
32
|
+
const name = tok.replace(/[^\w.+-]/g, "");
|
|
33
|
+
if (name && preinstalled.has(name)) redundant.add(name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (redundant.size > 0) {
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
checkId: "GHA055",
|
|
39
|
+
severity: "info",
|
|
40
|
+
message: `Job "${job}" installs ${[...redundant].join(", ")} at runtime, but GitHub-hosted runners already ship ${redundant.size > 1 ? "these tools" : "this tool"}. Drop the redundant install (or rely on the preinstalled version) to reduce supply-chain surface.`,
|
|
41
|
+
entity: job,
|
|
42
|
+
lexicon: "github",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return diagnostics;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { gha056 } from "./gha056";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["github", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["github", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("GHA056: workflow without a name", () => {
|
|
20
|
+
test("flags a workflow with no name", () => {
|
|
21
|
+
const yaml = `on:
|
|
22
|
+
push:
|
|
23
|
+
jobs:
|
|
24
|
+
build:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- run: echo build
|
|
28
|
+
`;
|
|
29
|
+
const diags = gha056.check(makeCtx(yaml));
|
|
30
|
+
expect(diags).toHaveLength(1);
|
|
31
|
+
expect(diags[0].checkId).toBe("GHA056");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("does not flag a named workflow", () => {
|
|
35
|
+
const yaml = `name: CI
|
|
36
|
+
on:
|
|
37
|
+
push:
|
|
38
|
+
jobs:
|
|
39
|
+
build:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- run: echo build
|
|
43
|
+
`;
|
|
44
|
+
const diags = gha056.check(makeCtx(yaml));
|
|
45
|
+
expect(diags).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA056: Workflow Without a `name:`
|
|
3
|
+
*
|
|
4
|
+
* Flags an emitted workflow with no top-level `name:`. Without one, GitHub falls
|
|
5
|
+
* back to the file path in the Actions UI and audit logs, making runs harder to
|
|
6
|
+
* identify and review. A clear name improves observability and audit output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import { getPrimaryOutput, extractWorkflowName } from "./yaml-helpers";
|
|
11
|
+
|
|
12
|
+
export const gha056: PostSynthCheck = {
|
|
13
|
+
id: "GHA056",
|
|
14
|
+
description: "Workflow without a name",
|
|
15
|
+
|
|
16
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
17
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
18
|
+
|
|
19
|
+
for (const [lexicon, output] of ctx.outputs) {
|
|
20
|
+
const yaml = getPrimaryOutput(output);
|
|
21
|
+
// Only inspect things that look like workflows (have a jobs: block).
|
|
22
|
+
if (!/^jobs:\s*$/m.test(yaml)) continue;
|
|
23
|
+
const name = extractWorkflowName(yaml);
|
|
24
|
+
if (!name) {
|
|
25
|
+
diagnostics.push({
|
|
26
|
+
checkId: "GHA056",
|
|
27
|
+
severity: "info",
|
|
28
|
+
message: `Workflow "${lexicon}" has no top-level name: — add one so runs are identifiable in the Actions UI and audit logs.`,
|
|
29
|
+
entity: "workflow",
|
|
30
|
+
lexicon: "github",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return diagnostics;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
|
+
import { gha057 } from "./gha057";
|
|
5
|
+
|
|
6
|
+
function makeCtx(dependabotYaml: string): PostSynthContext {
|
|
7
|
+
const output: SerializerResult = { primary: "", files: { "dependabot.yml": dependabotYaml } };
|
|
8
|
+
return {
|
|
9
|
+
outputs: new Map([["github", output]]),
|
|
10
|
+
entities: new Map(),
|
|
11
|
+
buildResult: {
|
|
12
|
+
outputs: new Map([["github", output]]),
|
|
13
|
+
entities: new Map(),
|
|
14
|
+
warnings: [],
|
|
15
|
+
errors: [],
|
|
16
|
+
sourceFileCount: 1,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("GHA057: dependency update executing untrusted code", () => {
|
|
22
|
+
test("flags insecure-external-code-execution: allow", () => {
|
|
23
|
+
const yaml = `version: 2
|
|
24
|
+
|
|
25
|
+
updates:
|
|
26
|
+
- package-ecosystem: npm
|
|
27
|
+
directory: /
|
|
28
|
+
schedule:
|
|
29
|
+
interval: weekly
|
|
30
|
+
insecure-external-code-execution: allow
|
|
31
|
+
`;
|
|
32
|
+
const diags = gha057.check(makeCtx(yaml));
|
|
33
|
+
expect(diags).toHaveLength(1);
|
|
34
|
+
expect(diags[0].checkId).toBe("GHA057");
|
|
35
|
+
expect(diags[0].severity).toBe("error");
|
|
36
|
+
expect(diags[0].entity).toBe("npm");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("does not flag deny", () => {
|
|
40
|
+
const yaml = `version: 2
|
|
41
|
+
|
|
42
|
+
updates:
|
|
43
|
+
- package-ecosystem: npm
|
|
44
|
+
directory: /
|
|
45
|
+
schedule:
|
|
46
|
+
interval: weekly
|
|
47
|
+
insecure-external-code-execution: deny
|
|
48
|
+
`;
|
|
49
|
+
const diags = gha057.check(makeCtx(yaml));
|
|
50
|
+
expect(diags).toHaveLength(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("does not flag a workflow-only output", () => {
|
|
54
|
+
const ctx: PostSynthContext = {
|
|
55
|
+
outputs: new Map([["github", "name: CI\non:\n push:\n"]]),
|
|
56
|
+
entities: new Map(),
|
|
57
|
+
buildResult: { outputs: new Map(), entities: new Map(), warnings: [], errors: [], sourceFileCount: 1 },
|
|
58
|
+
};
|
|
59
|
+
expect(gha057.check(ctx)).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA057: Dependency Update Executing Untrusted External Code
|
|
3
|
+
*
|
|
4
|
+
* Flags a dependabot `updates:` entry with
|
|
5
|
+
* `insecure-external-code-execution: allow`. That setting lets a freshly-pulled
|
|
6
|
+
* dependency run lifecycle scripts (e.g. npm `postinstall`) during the update
|
|
7
|
+
* itself — a compromised release executes in the update job before any review.
|
|
8
|
+
* Set it to `deny`, or isolate the ecosystem.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
|
+
import { getDependabotYaml, extractDependabotUpdates } from "./yaml-helpers";
|
|
13
|
+
|
|
14
|
+
export const gha057: PostSynthCheck = {
|
|
15
|
+
id: "GHA057",
|
|
16
|
+
description: "Dependency update allows executing untrusted external code",
|
|
17
|
+
|
|
18
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
19
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [, output] of ctx.outputs) {
|
|
22
|
+
const dependabot = getDependabotYaml(output);
|
|
23
|
+
if (!dependabot) continue;
|
|
24
|
+
for (const update of extractDependabotUpdates(dependabot)) {
|
|
25
|
+
if (update["insecure-external-code-execution"] === "allow") {
|
|
26
|
+
const eco = String(update["package-ecosystem"] ?? "?");
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
checkId: "GHA057",
|
|
29
|
+
severity: "error",
|
|
30
|
+
message: `Dependabot update for "${eco}" sets insecure-external-code-execution: allow, running dependency lifecycle scripts during the update. Set it to deny.`,
|
|
31
|
+
entity: eco,
|
|
32
|
+
lexicon: "github",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return diagnostics;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
|
+
import { gha058 } from "./gha058";
|
|
5
|
+
|
|
6
|
+
function makeCtx(dependabotYaml: string): PostSynthContext {
|
|
7
|
+
const output: SerializerResult = { primary: "", files: { "dependabot.yml": dependabotYaml } };
|
|
8
|
+
return {
|
|
9
|
+
outputs: new Map([["github", output]]),
|
|
10
|
+
entities: new Map(),
|
|
11
|
+
buildResult: {
|
|
12
|
+
outputs: new Map([["github", output]]),
|
|
13
|
+
entities: new Map(),
|
|
14
|
+
warnings: [],
|
|
15
|
+
errors: [],
|
|
16
|
+
sourceFileCount: 1,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("GHA058: dependency update without cooldown", () => {
|
|
22
|
+
test("flags an update with no cooldown", () => {
|
|
23
|
+
const yaml = `version: 2
|
|
24
|
+
|
|
25
|
+
updates:
|
|
26
|
+
- package-ecosystem: npm
|
|
27
|
+
directory: /
|
|
28
|
+
schedule:
|
|
29
|
+
interval: weekly
|
|
30
|
+
`;
|
|
31
|
+
const diags = gha058.check(makeCtx(yaml));
|
|
32
|
+
expect(diags).toHaveLength(1);
|
|
33
|
+
expect(diags[0].checkId).toBe("GHA058");
|
|
34
|
+
expect(diags[0].entity).toBe("npm");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("does not flag an update with a cooldown window", () => {
|
|
38
|
+
const yaml = `version: 2
|
|
39
|
+
|
|
40
|
+
updates:
|
|
41
|
+
- package-ecosystem: npm
|
|
42
|
+
directory: /
|
|
43
|
+
schedule:
|
|
44
|
+
interval: weekly
|
|
45
|
+
cooldown:
|
|
46
|
+
default-days: 7
|
|
47
|
+
`;
|
|
48
|
+
const diags = gha058.check(makeCtx(yaml));
|
|
49
|
+
expect(diags).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("flags an all-zero cooldown", () => {
|
|
53
|
+
const yaml = `version: 2
|
|
54
|
+
|
|
55
|
+
updates:
|
|
56
|
+
- package-ecosystem: pip
|
|
57
|
+
directory: /
|
|
58
|
+
schedule:
|
|
59
|
+
interval: daily
|
|
60
|
+
cooldown:
|
|
61
|
+
default-days: 0
|
|
62
|
+
`;
|
|
63
|
+
const diags = gha058.check(makeCtx(yaml));
|
|
64
|
+
expect(diags).toHaveLength(1);
|
|
65
|
+
expect(diags[0].entity).toBe("pip");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA058: Dependency Update Without a Cooldown
|
|
3
|
+
*
|
|
4
|
+
* Flags a dependabot `updates:` entry with no cooldown (or an all-zero one). A
|
|
5
|
+
* version published seconds ago — including a compromised one — is adopted
|
|
6
|
+
* immediately, with no window for the ecosystem to yank it or for anyone to
|
|
7
|
+
* react. Configure a `cooldown:` so freshly-published versions wait before they
|
|
8
|
+
* are proposed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
|
+
import { getDependabotYaml, extractDependabotUpdates } from "./yaml-helpers";
|
|
13
|
+
|
|
14
|
+
const COOLDOWN_DAY_KEYS = ["default-days", "semver-major-days", "semver-minor-days", "semver-patch-days"];
|
|
15
|
+
|
|
16
|
+
function hasEffectiveCooldown(cooldown: unknown): boolean {
|
|
17
|
+
if (!cooldown || typeof cooldown !== "object") return false;
|
|
18
|
+
const c = cooldown as Record<string, unknown>;
|
|
19
|
+
return COOLDOWN_DAY_KEYS.some((k) => typeof c[k] === "number" && (c[k] as number) > 0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const gha058: PostSynthCheck = {
|
|
23
|
+
id: "GHA058",
|
|
24
|
+
description: "Dependency update has no cooldown window",
|
|
25
|
+
|
|
26
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
for (const [, output] of ctx.outputs) {
|
|
30
|
+
const dependabot = getDependabotYaml(output);
|
|
31
|
+
if (!dependabot) continue;
|
|
32
|
+
for (const update of extractDependabotUpdates(dependabot)) {
|
|
33
|
+
if (!hasEffectiveCooldown(update.cooldown)) {
|
|
34
|
+
const eco = String(update["package-ecosystem"] ?? "?");
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
checkId: "GHA058",
|
|
37
|
+
severity: "warning",
|
|
38
|
+
message: `Dependabot update for "${eco}" has no cooldown — a version published moments ago is adopted immediately. Add a cooldown window so fresh (possibly compromised) releases wait before they are proposed.`,
|
|
39
|
+
entity: eco,
|
|
40
|
+
lexicon: "github",
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return diagnostics;
|
|
47
|
+
},
|
|
48
|
+
};
|