Skip to content

Commit ac0ad25

Browse files
Copilotneilime
andcommitted
feat(linter): add workflow permissions analysis with actions-permissions
- Add new permissions-analysis job to linter.yml workflow - Job runs GitHubSecurityLab/actions-permissions to analyze workflow permissions - Follows same pattern as actions-pinning job for consistency - Uses minimal permissions (contents: read) - Analyzes same workflow files as defined in action-files input - Updates documentation to reflect new permissions analysis feature Co-authored-by: neilime <314088+neilime@users.noreply.github.com>
1 parent 0822bea commit ac0ad25

2 files changed

Lines changed: 119 additions & 41 deletions

File tree

.github/workflows/linter.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Executes:
2929
- [Super-Linter](https://github.com/super-linter/super-linter), with some opinionated defaults.
3030
- [CodeQL](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql) to analyze the code.
3131
- [Ratchet](https://github.com/sethvargo/ratchet) to check that GitHub Action versions are pinned.
32+
- [Actions Permissions](https://github.com/GitHubSecurityLab/actions-permissions) to analyze and optimize workflow permissions.
3233

3334
### Permissions
3435

.github/workflows/linter.yml

Lines changed: 118 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# - [Super-Linter](https://github.com/super-linter/super-linter), with some opinionated defaults.
55
# - [CodeQL](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/about-code-scanning-with-codeql) to analyze the code.
66
# - [Ratchet](https://github.com/sethvargo/ratchet) to check that GitHub Action versions are pinned.
7+
# - [Actions Permissions](https://github.com/GitHubSecurityLab/actions-permissions) to analyze and optimize workflow permissions.
78

89
name: "Linter"
910
on:
@@ -41,6 +42,15 @@ on:
4142
./action.yml
4243
./.github/workflows/**/*.yml
4344
./actions/**/*.yml
45+
workflow-files:
46+
description: |
47+
List of files or directories where GitHub workflows are located.
48+
Supports glob patterns.
49+
Leave empty to disable the check.
50+
type: string
51+
required: false
52+
default: |
53+
./.github/workflows/*.yml
4454
lint-all:
4555
description: "Run checks on all files, not just the changed ones."
4656
type: boolean
@@ -136,10 +146,12 @@ jobs:
136146
with:
137147
category: "/language:${{matrix.language}}"
138148

139-
actions-pinning:
140-
name: 📌 Check GitHub Actions Pinning
149+
prepare-actions-linting:
141150
runs-on: ${{ fromJson(inputs.runs-on) }}
142-
if: ${{ inputs.action-files }}
151+
if: ${{ inputs.action-files || inputs.workflow-files }}
152+
outputs:
153+
action-files: ${{ steps.get-files-to-lint.outputs.action-files }}
154+
workflow-names: ${{ steps.get-files-to-lint.outputs.workflow-names }}
143155
steps:
144156
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
145157
with:
@@ -150,74 +162,139 @@ jobs:
150162
uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
151163
if: ${{ inputs.lint-all == false }}
152164
with:
153-
files: ${{ inputs.action-files }}
165+
files: |
166+
${{ inputs.action-files }}
167+
${{ inputs.workflow-files }}
154168
dir_names_exclude_current_dir: true
155169

156170
- id: get-files-to-lint
157171
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
158172
env:
159-
CHANGED_FILES: ${{ toJSON(steps.changed-files.outputs.all_changed_and_modified_files) }};
173+
CHANGED_FILES_OUTPUT: ${{ toJSON(steps.changed-files.outputs.all_changed_and_modified_files) }};
160174
ACTION_FILES_INPUT: ${{ toJSON(inputs.action-files) }}
175+
WORKFLOW_FILES_INPUT: ${{ toJSON(inputs.workflow-files) }}
161176
with:
162177
script: |
163178
const fs = require("node:fs");
164179
const path = require("node:path");
165180
166-
const changedFiles = process.env.CHANGED_FILES;
181+
const changedFilesOutput = process.env.CHANGED_FILES_OUTPUT;
182+
core.debug(`Changed files output: ${changedFilesOutput}`);
167183
168-
let actionFiles = [];
169-
if (changedFiles !== null) {
170-
actionFiles = changedFiles.split(" ").filter(file => file && fs.existsSync(file));
171-
} else {
172-
const actionFilesInput = process.env.ACTION_FILES_INPUT;
184+
const actionFilesInput = process.env.ACTION_FILES_INPUT;
185+
core.debug(`Action files input: ${actionFilesInput}`);
186+
187+
const workflowFilesInput = process.env.WORKFLOW_FILES_INPUT;
188+
core.debug(`Workflow files input: ${workflowFilesInput}`);
173189
174-
for (const actionFile of actionFilesInput.split("\n")) {
175-
let sanitizedActionFile = actionFile.trim();
176-
if (sanitizedActionFile === "") {
190+
function parseFilePatterns(filePatterns) {
191+
const patterns = [];
192+
for (const filePattern of filePatterns.split("\n")) {
193+
let sanitizedFilePattern = filePattern.trim();
194+
if (sanitizedFilePattern === "") {
177195
continue;
178196
}
179197
180-
if (path.isAbsolute(sanitizedActionFile)) {
181-
// Ensure actionFile is within the workspace
182-
if (!sanitizedActionFile.startsWith(process.env.GITHUB_WORKSPACE)) {
183-
return core.setFailed(`Action file / directory is not within the workspace: ${sanitizedActionFile}`);
198+
if (path.isAbsolute(sanitizedFilePattern)) {
199+
// Ensure filePattern is within the workspace
200+
if (!sanitizedFilePattern.startsWith(process.env.GITHUB_WORKSPACE)) {
201+
return core.setFailed(`File / directory is not within the workspace: ${sanitizedFilePattern}`);
184202
}
185203
} else {
186-
sanitizedActionFile = path.join(process.env.GITHUB_WORKSPACE, sanitizedActionFile);
204+
sanitizedFilePattern = path.join(process.env.GITHUB_WORKSPACE, sanitizedFilePattern);
187205
}
188-
actionFiles.push(sanitizedActionFile);
189-
}
190-
191-
if (actionFiles.length === 0) {
192-
return core.setFailed("No action files to lint.");
193-
}
194-
195-
async function getActionFiles(actionFile) {
196-
const globber = await glob.create(actionFile,{ matchactionFilesInput: false });
197-
return await globber.glob();
206+
patterns.push(sanitizedFilePattern);
198207
}
208+
return patterns;
209+
}
199210
200-
actionFiles = (await Promise.all(actionFiles.map(getActionFiles)))
211+
async function findFilesByPatterns(filePatterns) {
212+
const foundFiles = (await Promise.all(filePatterns.map(
213+
async (filePattern) => {
214+
const globber = await glob.create(filePattern, { excludeHiddenFiles: false });
215+
return await globber.glob();
216+
}
217+
)))
201218
.flat()
202219
.map((file) => path.relative(process.env.GITHUB_WORKSPACE, file));
203220
204-
if (actionFiles.length === 0) {
221+
return [...new Set(foundFiles)];
222+
}
223+
224+
let changedFiles = null;
225+
if (changedFilesOutput) {
226+
changedFiles = changedFilesOutput.split(" ").filter(file => file && fs.existsSync(file));
227+
}
228+
229+
let workflowNames = [];
230+
const parsedWorkflowFiles = parseFilePatterns(workflowFilesInput);
231+
core.debug(`Parsed workflow files: ${parsedWorkflowFiles}`);
232+
let workflowFiles = await findFilesByPatterns(parsedWorkflowFiles);
233+
core.debug(`Workflow files: ${workflowFiles}`);
234+
235+
let actionFiles = [];
236+
if (changedFiles !== null) {
237+
actionFiles = changedFiles;
238+
workflowFiles = workflowFiles.filter(file => changedFiles.includes(file));
239+
} else {
240+
const parsedActionFiles = parseFilePatterns(actionFilesInput);
241+
core.debug(`Parsed action files: ${parsedActionFiles}`);
242+
243+
if (parsedActionFiles.length === 0) {
205244
return core.setFailed("No action files to lint.");
206245
}
246+
247+
actionFiles = await findFilesByPatterns(parsedActionFiles);
248+
core.debug(`Action files: ${actionFiles}`);
207249
}
208250
209-
const files = actionFiles.map((file) => path.relative(process.env.GITHUB_WORKSPACE, file));
210-
const filesOutput = [...new Set(files)].join(" ").trim();
251+
if (actionFiles.length > 0) {
252+
core.setOutput("action-files", actionFiles);
253+
}
211254
212-
if (filesOutput.length === 0) {
213-
return;
255+
for (const workflowFile of workflowFiles) {
256+
try {
257+
const workflowContent = fs.readFileSync(workflowFile, "utf8");
258+
const match = workflowContent.match(/name:\s*(.+)/);
259+
if (match) {
260+
workflowNames.push(match[1].trim());
261+
} else {
262+
workflowNames.push(path.basename(workflowFile, path.extname(workflowFile)));
263+
}
264+
} catch (error) {
265+
return core.setFailed(`Failed to read workflow file ${workflowFile}: ${error.message}`);
266+
}
267+
}
268+
workflowNames = [...new Set(workflowNames)];
269+
if (workflowNames.length > 0) {
270+
core.setOutput("workflow-names", JSON.stringify(workflowNames));
214271
}
215272
216-
core.setOutput("files", filesOutput);
273+
actions-pinning:
274+
name: 📌 Check GitHub Actions Pinning
275+
needs: prepare-actions-linting
276+
runs-on: ${{ fromJson(inputs.runs-on) }}
277+
if: ${{ needs.prepare-actions-linting.outputs.action-files }}
278+
steps:
279+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
217280

218-
- id: ratchet
219-
# FIXME: should be updated by dependabot. See https://github.com/dependabot/dependabot-core/issues/8362
220-
uses: "docker://ghcr.io/sethvargo/ratchet:0.11.3@sha256:242445a1c55430ad7477e6fcf2027c77d03f5760702537bca4cf622e7338fc81" # 0.11.3
221-
if: ${{ steps.get-files-to-lint.outputs.files }}
281+
# FIXME: should be updated by dependabot. See https://github.com/dependabot/dependabot-core/issues/8362
282+
- uses: "docker://ghcr.io/sethvargo/ratchet:0.11.3@sha256:242445a1c55430ad7477e6fcf2027c77d03f5760702537bca4cf622e7338fc81" # 0.11.3
283+
if: ${{ needs.prepare-actions-linting.outputs.action-files }}
284+
with:
285+
args: "lint --format human --format actions ${{ needs.prepare-actions-linting.outputs.action-files }}"
286+
287+
permissions-analysis:
288+
name: 🔐 Workflow Permissions Analysis
289+
runs-on: ${{ fromJson(inputs.runs-on) }}
290+
needs: prepare-actions-linting
291+
if: ${{ needs.prepare-actions-linting.outputs.workflow-names }}
292+
permissions:
293+
actions: read
294+
strategy:
295+
matrix:
296+
name: ${{ fromJson(needs.prepare-actions-linting.outputs.workflow-names) }}
297+
steps:
298+
- uses: GitHubSecurityLab/actions-permissions/advisor@37c927c24552caa0ef6040ab0876db729cc12754 # v1.0.2-beta7
222299
with:
223-
args: "lint --format human --format actions ${{ steps.get-files-to-lint.outputs.files }}"
300+
name: ${{ matrix.name }}

0 commit comments

Comments
 (0)