@intentius/chant-lexicon-gitlab 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 +25 -5
- package/dist/manifest.json +1 -1
- package/dist/rules/wgl029.ts +59 -0
- package/dist/rules/wgl030.ts +48 -0
- package/dist/rules/wgl031.ts +37 -0
- package/dist/rules/wgl032.ts +68 -0
- package/dist/rules/wgl033.ts +38 -0
- package/dist/rules/wgl034.ts +42 -0
- package/dist/rules/wgl035.ts +44 -0
- package/dist/rules/wgl036.ts +44 -0
- package/dist/rules/wgl037.ts +47 -0
- package/dist/rules/wgl038.ts +48 -0
- package/dist/rules/wgl039.ts +41 -0
- package/dist/rules/wgl040.ts +42 -0
- package/dist/rules/wgl041.ts +57 -0
- package/dist/rules/wgl042.ts +46 -0
- package/dist/rules/wgl043.ts +45 -0
- package/dist/rules/wgl044.ts +41 -0
- package/dist/rules/wgl045.ts +57 -0
- package/dist/rules/wgl046.ts +43 -0
- package/dist/rules/wgl047.ts +41 -0
- package/dist/rules/wgl048.ts +37 -0
- package/dist/rules/yaml-helpers.ts +214 -0
- package/dist/skills/chant-gitlab-migrate.md +10 -0
- package/dist/skills/chant-gitlab-patterns.md +15 -0
- package/package.json +1 -1
- package/src/codegen/docs.ts +251 -2
- package/src/composites/composites.test.ts +58 -0
- package/src/composites/index.ts +2 -0
- package/src/composites/mr-plan-report.ts +82 -0
- package/src/index.ts +2 -2
- package/src/lint/post-synth/wgl029.test.ts +77 -0
- package/src/lint/post-synth/wgl029.ts +59 -0
- package/src/lint/post-synth/wgl030.test.ts +57 -0
- package/src/lint/post-synth/wgl030.ts +48 -0
- package/src/lint/post-synth/wgl031.test.ts +66 -0
- package/src/lint/post-synth/wgl031.ts +37 -0
- package/src/lint/post-synth/wgl032.test.ts +53 -0
- package/src/lint/post-synth/wgl032.ts +68 -0
- package/src/lint/post-synth/wgl033.test.ts +58 -0
- package/src/lint/post-synth/wgl033.ts +38 -0
- package/src/lint/post-synth/wgl034.test.ts +60 -0
- package/src/lint/post-synth/wgl034.ts +42 -0
- package/src/lint/post-synth/wgl035.test.ts +42 -0
- package/src/lint/post-synth/wgl035.ts +44 -0
- package/src/lint/post-synth/wgl036.test.ts +49 -0
- package/src/lint/post-synth/wgl036.ts +44 -0
- package/src/lint/post-synth/wgl037.test.ts +45 -0
- package/src/lint/post-synth/wgl037.ts +47 -0
- package/src/lint/post-synth/wgl038.test.ts +54 -0
- package/src/lint/post-synth/wgl038.ts +48 -0
- package/src/lint/post-synth/wgl039.test.ts +39 -0
- package/src/lint/post-synth/wgl039.ts +41 -0
- package/src/lint/post-synth/wgl040.test.ts +48 -0
- package/src/lint/post-synth/wgl040.ts +42 -0
- package/src/lint/post-synth/wgl041.test.ts +49 -0
- package/src/lint/post-synth/wgl041.ts +57 -0
- package/src/lint/post-synth/wgl042.test.ts +45 -0
- package/src/lint/post-synth/wgl042.ts +46 -0
- package/src/lint/post-synth/wgl043.test.ts +43 -0
- package/src/lint/post-synth/wgl043.ts +45 -0
- package/src/lint/post-synth/wgl044.test.ts +46 -0
- package/src/lint/post-synth/wgl044.ts +41 -0
- package/src/lint/post-synth/wgl045.test.ts +48 -0
- package/src/lint/post-synth/wgl045.ts +57 -0
- package/src/lint/post-synth/wgl046.test.ts +67 -0
- package/src/lint/post-synth/wgl046.ts +43 -0
- package/src/lint/post-synth/wgl047.test.ts +40 -0
- package/src/lint/post-synth/wgl047.ts +41 -0
- package/src/lint/post-synth/wgl048.test.ts +56 -0
- package/src/lint/post-synth/wgl048.ts +37 -0
- package/src/lint/post-synth/yaml-helpers.ts +214 -0
- package/src/lint/rules/data/known-include-sources.ts +41 -0
- package/src/lint/rules/data/untrusted-variables.ts +31 -0
- package/src/mcp/context-tools.test.ts +178 -0
- package/src/mcp/context-tools.ts +282 -0
- package/src/migrate/from-github/diagnostics.ts +18 -0
- package/src/migrate/from-github/index.ts +20 -1
- package/src/migrate/from-github/provenance.ts +28 -0
- package/src/migrate/from-github/rules.ts +7 -0
- package/src/migrate/from-github/security.test.ts +98 -0
- package/src/migrate/from-github/security.ts +208 -0
- package/src/plugin.test.ts +30 -1
- package/src/plugin.ts +4 -0
- package/src/skills/chant-gitlab-migrate.md +10 -0
- package/src/skills/chant-gitlab-patterns.md +15 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { wgl044 } from "./wgl044";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("WGL044: public artifacts", () => {
|
|
20
|
+
test("flags artifacts:public: true", () => {
|
|
21
|
+
const yaml = `build:
|
|
22
|
+
artifacts:
|
|
23
|
+
public: true
|
|
24
|
+
paths:
|
|
25
|
+
- dist/
|
|
26
|
+
script:
|
|
27
|
+
- make
|
|
28
|
+
`;
|
|
29
|
+
const diags = wgl044.check(makeCtx(yaml));
|
|
30
|
+
expect(diags).toHaveLength(1);
|
|
31
|
+
expect(diags[0].checkId).toBe("WGL044");
|
|
32
|
+
expect(diags[0].entity).toBe("build");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("does not flag private artifacts", () => {
|
|
36
|
+
const yaml = `build:
|
|
37
|
+
artifacts:
|
|
38
|
+
paths:
|
|
39
|
+
- dist/
|
|
40
|
+
script:
|
|
41
|
+
- make
|
|
42
|
+
`;
|
|
43
|
+
const diags = wgl044.check(makeCtx(yaml));
|
|
44
|
+
expect(diags).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGL044: Public Artifacts
|
|
3
|
+
*
|
|
4
|
+
* Flags `artifacts:public: true`. Public artifacts are downloadable by anyone,
|
|
5
|
+
* including unauthenticated users for public projects, exposing build output and
|
|
6
|
+
* anything swept into it. Keep artifacts private unless they are deliberately
|
|
7
|
+
* meant to be world-readable.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { getPrimaryOutput, extractJobs, extractJobSection } from "./yaml-helpers";
|
|
12
|
+
|
|
13
|
+
const PUBLIC_ARTIFACTS = /^\s+public:\s*true\s*$/m;
|
|
14
|
+
|
|
15
|
+
export const wgl044: PostSynthCheck = {
|
|
16
|
+
id: "WGL044",
|
|
17
|
+
description: "Public artifacts expose build output",
|
|
18
|
+
|
|
19
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
20
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
21
|
+
|
|
22
|
+
for (const [, output] of ctx.outputs) {
|
|
23
|
+
const yaml = getPrimaryOutput(output);
|
|
24
|
+
for (const [job] of extractJobs(yaml)) {
|
|
25
|
+
const section = extractJobSection(yaml, job);
|
|
26
|
+
if (!section || !/^\s+artifacts:/m.test(section)) continue;
|
|
27
|
+
if (PUBLIC_ARTIFACTS.test(section)) {
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
checkId: "WGL044",
|
|
30
|
+
severity: "warning",
|
|
31
|
+
message: `Job "${job}" publishes public artifacts (artifacts:public: true), downloadable by anyone. Keep them private unless they are meant to be world-readable.`,
|
|
32
|
+
entity: job,
|
|
33
|
+
lexicon: "gitlab",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return diagnostics;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { wgl045 } from "./wgl045";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("WGL045: credential-bearing artifact path", () => {
|
|
20
|
+
test("flags an artifact path that captures .env", () => {
|
|
21
|
+
const yaml = `build:
|
|
22
|
+
artifacts:
|
|
23
|
+
paths:
|
|
24
|
+
- dist/
|
|
25
|
+
- .env
|
|
26
|
+
script:
|
|
27
|
+
- make
|
|
28
|
+
`;
|
|
29
|
+
const diags = wgl045.check(makeCtx(yaml));
|
|
30
|
+
expect(diags).toHaveLength(1);
|
|
31
|
+
expect(diags[0].checkId).toBe("WGL045");
|
|
32
|
+
expect(diags[0].severity).toBe("error");
|
|
33
|
+
expect(diags[0].message).toContain(".env");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("does not flag ordinary build output paths", () => {
|
|
37
|
+
const yaml = `build:
|
|
38
|
+
artifacts:
|
|
39
|
+
paths:
|
|
40
|
+
- dist/
|
|
41
|
+
- build/output.js
|
|
42
|
+
script:
|
|
43
|
+
- make
|
|
44
|
+
`;
|
|
45
|
+
const diags = wgl045.check(makeCtx(yaml));
|
|
46
|
+
expect(diags).toHaveLength(0);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGL045: Credential-Bearing Artifact Path
|
|
3
|
+
*
|
|
4
|
+
* Flags an `artifacts:paths:` entry that looks like it would sweep a credential
|
|
5
|
+
* or sensitive file into the artifact (`.env`, `*.pem`, `id_rsa`, `*.key`,
|
|
6
|
+
* `.npmrc`, `.netrc`, `credentials`). Artifacts flow to later stages and are
|
|
7
|
+
* downloadable, so a captured credential leaks across the boundary. Exclude the
|
|
8
|
+
* sensitive file from the artifact path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
|
+
import { getPrimaryOutput, extractJobs, extractJobSection } from "./yaml-helpers";
|
|
13
|
+
|
|
14
|
+
const CREDENTIAL_PATH = /(^|\/)(\.env(\.|$)|\.npmrc|\.netrc|id_rsa|.*\.pem|.*\.key|.*\.p12|credentials?(\.|$))/i;
|
|
15
|
+
|
|
16
|
+
export const wgl045: PostSynthCheck = {
|
|
17
|
+
id: "WGL045",
|
|
18
|
+
description: "Artifact path that may capture a credential file",
|
|
19
|
+
|
|
20
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
21
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
22
|
+
|
|
23
|
+
for (const [, output] of ctx.outputs) {
|
|
24
|
+
const yaml = getPrimaryOutput(output);
|
|
25
|
+
for (const [job] of extractJobs(yaml)) {
|
|
26
|
+
const section = extractJobSection(yaml, job);
|
|
27
|
+
if (!section) continue;
|
|
28
|
+
// Collect the lines under the artifacts: block (deeper indent than the key).
|
|
29
|
+
const lines = section.split("\n");
|
|
30
|
+
const artifactsIdx = lines.findIndex((l) => /^\s+artifacts:\s*$/.test(l));
|
|
31
|
+
if (artifactsIdx === -1) continue;
|
|
32
|
+
const artifactsIndent = lines[artifactsIdx].search(/\S/);
|
|
33
|
+
const blockLines: string[] = [];
|
|
34
|
+
for (let i = artifactsIdx + 1; i < lines.length; i++) {
|
|
35
|
+
if (lines[i].trim() === "") continue;
|
|
36
|
+
if (lines[i].search(/\S/) <= artifactsIndent) break;
|
|
37
|
+
blockLines.push(lines[i]);
|
|
38
|
+
}
|
|
39
|
+
const hit = blockLines
|
|
40
|
+
.filter((l) => /^\s+-\s+/.test(l))
|
|
41
|
+
.map((l) => l.replace(/^\s+-\s+/, "").trim().replace(/^['"]|['"]$/g, ""))
|
|
42
|
+
.find((p) => CREDENTIAL_PATH.test(p));
|
|
43
|
+
if (hit) {
|
|
44
|
+
diagnostics.push({
|
|
45
|
+
checkId: "WGL045",
|
|
46
|
+
severity: "error",
|
|
47
|
+
message: `Job "${job}" includes a credential-like file ("${hit}") in its artifacts. Exclude it — artifacts are passed downstream and are downloadable.`,
|
|
48
|
+
entity: job,
|
|
49
|
+
lexicon: "gitlab",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return diagnostics;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { wgl046 } from "./wgl046";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("WGL046: cache populated in a merge-request pipeline", () => {
|
|
20
|
+
test("flags a cache write reachable from merge requests", () => {
|
|
21
|
+
const yaml = `build:
|
|
22
|
+
rules:
|
|
23
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
24
|
+
cache:
|
|
25
|
+
key: build-cache
|
|
26
|
+
paths:
|
|
27
|
+
- node_modules/
|
|
28
|
+
script:
|
|
29
|
+
- npm ci
|
|
30
|
+
`;
|
|
31
|
+
const diags = wgl046.check(makeCtx(yaml));
|
|
32
|
+
expect(diags).toHaveLength(1);
|
|
33
|
+
expect(diags[0].checkId).toBe("WGL046");
|
|
34
|
+
expect(diags[0].entity).toBe("build");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("does not flag a pull-only cache", () => {
|
|
38
|
+
const yaml = `build:
|
|
39
|
+
rules:
|
|
40
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
41
|
+
cache:
|
|
42
|
+
key: build-cache
|
|
43
|
+
paths:
|
|
44
|
+
- node_modules/
|
|
45
|
+
policy: pull
|
|
46
|
+
script:
|
|
47
|
+
- npm ci
|
|
48
|
+
`;
|
|
49
|
+
const diags = wgl046.check(makeCtx(yaml));
|
|
50
|
+
expect(diags).toHaveLength(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("does not flag a cache on a protected-only job", () => {
|
|
54
|
+
const yaml = `build:
|
|
55
|
+
rules:
|
|
56
|
+
- if: $CI_COMMIT_BRANCH == "main"
|
|
57
|
+
cache:
|
|
58
|
+
key: build-cache
|
|
59
|
+
paths:
|
|
60
|
+
- node_modules/
|
|
61
|
+
script:
|
|
62
|
+
- npm ci
|
|
63
|
+
`;
|
|
64
|
+
const diags = wgl046.check(makeCtx(yaml));
|
|
65
|
+
expect(diags).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGL046: Cache Populated in a Merge-Request Pipeline (Poisoning Risk)
|
|
3
|
+
*
|
|
4
|
+
* Flags a job that writes a cache (`cache:` with a push policy) and is reachable
|
|
5
|
+
* from merge-request pipelines. An outside contributor's MR can populate the
|
|
6
|
+
* cache; a later protected run that restores the same key then executes
|
|
7
|
+
* attacker-influenced contents. Restrict cache writes to protected refs, or
|
|
8
|
+
* scope cache keys so MR pipelines cannot write entries a protected run reads.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
12
|
+
import { getPrimaryOutput, extractJobs, extractJobSection, isMergeRequestReachable } from "./yaml-helpers";
|
|
13
|
+
|
|
14
|
+
export const wgl046: PostSynthCheck = {
|
|
15
|
+
id: "WGL046",
|
|
16
|
+
description: "Cache populated in a merge-request pipeline (poisoning risk)",
|
|
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] of extractJobs(yaml)) {
|
|
24
|
+
const section = extractJobSection(yaml, job);
|
|
25
|
+
if (!section || !/^\s+cache:/m.test(section)) continue;
|
|
26
|
+
if (!isMergeRequestReachable(section)) continue;
|
|
27
|
+
// pull-only caches cannot poison; flag push/pull-push (the default when policy is absent)
|
|
28
|
+
const policyMatch = section.match(/^\s+policy:\s*(\S+)/m);
|
|
29
|
+
const policy = policyMatch ? policyMatch[1].replace(/^['"]|['"]$/g, "") : "pull-push";
|
|
30
|
+
if (policy === "pull") continue;
|
|
31
|
+
diagnostics.push({
|
|
32
|
+
checkId: "WGL046",
|
|
33
|
+
severity: "warning",
|
|
34
|
+
message: `Job "${job}" writes a cache and is reachable from merge-request pipelines. An MR can poison the cache for a later protected run that restores the same key — restrict cache writes to protected refs or scope the key.`,
|
|
35
|
+
entity: job,
|
|
36
|
+
lexicon: "gitlab",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return diagnostics;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { wgl047 } from "./wgl047";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("WGL047: curl | bash runtime execution", () => {
|
|
20
|
+
test("flags curl piped to bash", () => {
|
|
21
|
+
const yaml = `build:
|
|
22
|
+
script:
|
|
23
|
+
- curl -sSL https://example.com/install.sh | bash
|
|
24
|
+
`;
|
|
25
|
+
const diags = wgl047.check(makeCtx(yaml));
|
|
26
|
+
expect(diags).toHaveLength(1);
|
|
27
|
+
expect(diags[0].checkId).toBe("WGL047");
|
|
28
|
+
expect(diags[0].entity).toBe("build");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("does not flag download-then-run on separate lines", () => {
|
|
32
|
+
const yaml = `build:
|
|
33
|
+
script:
|
|
34
|
+
- curl -sSLo install.sh https://example.com/install.sh
|
|
35
|
+
- bash install.sh
|
|
36
|
+
`;
|
|
37
|
+
const diags = wgl047.check(makeCtx(yaml));
|
|
38
|
+
expect(diags).toHaveLength(0);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGL047: Software Fetched and Executed at Runtime Without Verification
|
|
3
|
+
*
|
|
4
|
+
* Flags a `script:` command that pipes a network download straight into a shell
|
|
5
|
+
* (`curl ... | bash`, `wget ... | sh`). The fetched script is unpinned and
|
|
6
|
+
* unverified, so whoever controls the URL controls what runs in the job.
|
|
7
|
+
* Download to a file, verify a checksum/signature, then execute it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { getPrimaryOutput, extractScriptCommands } from "./yaml-helpers";
|
|
12
|
+
|
|
13
|
+
const PIPE_TO_SHELL = /\b(curl|wget)\b[^\n|]*\|\s*(sudo\s+)?(ba)?sh\b/i;
|
|
14
|
+
|
|
15
|
+
export const wgl047: PostSynthCheck = {
|
|
16
|
+
id: "WGL047",
|
|
17
|
+
description: "Software fetched and piped to a shell without verification",
|
|
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, command } of extractScriptCommands(yaml)) {
|
|
26
|
+
if (PIPE_TO_SHELL.test(command) && !seen.has(job)) {
|
|
27
|
+
seen.add(job);
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
checkId: "WGL047",
|
|
30
|
+
severity: "warning",
|
|
31
|
+
message: `Job "${job}" pipes a network download directly into a shell — the fetched code is unpinned and unverified. Download to a file, verify a checksum or signature, then execute it.`,
|
|
32
|
+
entity: job,
|
|
33
|
+
lexicon: "gitlab",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return diagnostics;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
|
+
import { wgl048 } from "./wgl048";
|
|
4
|
+
|
|
5
|
+
function makeCtx(yaml: string): PostSynthContext {
|
|
6
|
+
return {
|
|
7
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
8
|
+
entities: new Map(),
|
|
9
|
+
buildResult: {
|
|
10
|
+
outputs: new Map([["gitlab", yaml]]),
|
|
11
|
+
entities: new Map(),
|
|
12
|
+
warnings: [],
|
|
13
|
+
errors: [],
|
|
14
|
+
sourceFileCount: 1,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("WGL048: pipeline without a name", () => {
|
|
20
|
+
test("flags a workflow: block with no name", () => {
|
|
21
|
+
const yaml = `workflow:
|
|
22
|
+
rules:
|
|
23
|
+
- if: $CI_COMMIT_BRANCH
|
|
24
|
+
|
|
25
|
+
build:
|
|
26
|
+
script:
|
|
27
|
+
- make
|
|
28
|
+
`;
|
|
29
|
+
const diags = wgl048.check(makeCtx(yaml));
|
|
30
|
+
expect(diags).toHaveLength(1);
|
|
31
|
+
expect(diags[0].checkId).toBe("WGL048");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("does not flag a named workflow", () => {
|
|
35
|
+
const yaml = `workflow:
|
|
36
|
+
name: My Pipeline
|
|
37
|
+
rules:
|
|
38
|
+
- if: $CI_COMMIT_BRANCH
|
|
39
|
+
|
|
40
|
+
build:
|
|
41
|
+
script:
|
|
42
|
+
- make
|
|
43
|
+
`;
|
|
44
|
+
const diags = wgl048.check(makeCtx(yaml));
|
|
45
|
+
expect(diags).toHaveLength(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("does not flag a pipeline with no workflow block", () => {
|
|
49
|
+
const yaml = `build:
|
|
50
|
+
script:
|
|
51
|
+
- make
|
|
52
|
+
`;
|
|
53
|
+
const diags = wgl048.check(makeCtx(yaml));
|
|
54
|
+
expect(diags).toHaveLength(0);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WGL048: Pipeline Without a Name
|
|
3
|
+
*
|
|
4
|
+
* Flags a pipeline that defines a `workflow:` block but no `workflow:name:`.
|
|
5
|
+
* A pipeline name shows up in the GitLab UI and audit output; naming pipelines
|
|
6
|
+
* makes runs easier to identify and review. Only flags pipelines that already
|
|
7
|
+
* customize `workflow:` (where a name is cheap to add and most useful).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
11
|
+
import { getPrimaryOutput } from "./yaml-helpers";
|
|
12
|
+
|
|
13
|
+
export const wgl048: PostSynthCheck = {
|
|
14
|
+
id: "WGL048",
|
|
15
|
+
description: "Pipeline defines workflow: but no workflow:name",
|
|
16
|
+
|
|
17
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
18
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [, output] of ctx.outputs) {
|
|
21
|
+
const yaml = getPrimaryOutput(output);
|
|
22
|
+
const workflowSection = yaml.split("\n\n").find((s) => /^workflow:/.test(s));
|
|
23
|
+
if (!workflowSection) continue;
|
|
24
|
+
if (!/^\s+name:/m.test(workflowSection)) {
|
|
25
|
+
diagnostics.push({
|
|
26
|
+
checkId: "WGL048",
|
|
27
|
+
severity: "info",
|
|
28
|
+
message: `The pipeline defines a workflow: block but no workflow:name. Add a name so runs are identifiable in the GitLab UI and audit output.`,
|
|
29
|
+
entity: "workflow",
|
|
30
|
+
lexicon: "gitlab",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return diagnostics;
|
|
36
|
+
},
|
|
37
|
+
};
|