Skip to content

feat: new secscan action - #155

Draft
bepri wants to merge 6 commits into
mainfrom
work/secscan-action/CRAFT-5208
Draft

feat: new secscan action#155
bepri wants to merge 6 commits into
mainfrom
work/secscan-action/CRAFT-5208

Conversation

@bepri

@bepri bepri commented Jun 22, 2026

Copy link
Copy Markdown
Member

Creates a new action to automate secscan. The intended use-case is for this to run on new tags, or as a monthly workflow against latest/stable. You can see it in action here: canonical/snapcraft#6303

CRAFT-5208

@bepri bepri self-assigned this Jun 22, 2026
Copilot AI review requested due to automatic review settings June 22, 2026 18:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new secscan GitHub Action (Node/TypeScript) intended to run Canonical’s secscan-client against a provided snap, collect scanner outputs, and upload results as an artifact, alongside introducing a pnpm-based TypeScript tooling setup for the repo.

Changes:

  • Introduce the secscan action implementation (inputs parsing + scan execution + artifact upload) and a Vitest test suite.
  • Add monorepo JS tooling: pnpm workspace/lockfile, root TypeScript config, and ESLint flat config.
  • Update .gitignore for pnpm and (implicitly) for committing built action artifacts.

Reviewed changes

Copilot reviewed 7 out of 11 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tsconfig.json Adds TypeScript compiler configuration for the workspace.
secscan/src/index.ts Implements the action logic (input handling, scan orchestration, artifact upload).
secscan/tests/index.test.ts Adds unit tests for parsing/derivation and scanner exit-code handling.
secscan/action.yaml Declares the secscan GitHub Action interface (inputs/outputs/runtime).
secscan/package.json Defines package scripts/dependencies for building/testing the action.
package.json Adds root-level pnpm scripts and dev tool dependencies.
eslint.config.js Adds ESLint flat config with TypeScript support.
pnpm-workspace.yaml Configures pnpm workspace layout.
pnpm-lock.yaml Adds pnpm lockfile for reproducible installs.
.gitignore Updates ignore rules for pnpm (and changes handling of dist/).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread secscan/src/index.ts Outdated
Comment thread secscan/src/index.ts
Comment thread secscan/src/index.ts
Comment thread pnpm-lock.yaml Outdated
Comment thread secscan/package.json
@bepri
bepri force-pushed the work/secscan-action/CRAFT-5208 branch from 8c83b7a to a1cb5f9 Compare June 22, 2026 20:35
@bepri
bepri force-pushed the work/secscan-action/CRAFT-5208 branch from a1cb5f9 to 9ce60e4 Compare June 22, 2026 20:36
@bepri
bepri force-pushed the work/secscan-action/CRAFT-5208 branch from ef61dd3 to 540f7aa Compare June 23, 2026 14:33
@bepri
bepri force-pushed the work/secscan-action/CRAFT-5208 branch from 540f7aa to bd2db59 Compare June 23, 2026 14:50
Comment thread secscan/dist/index.js

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transpiled, flattened version of the code must be present on main for actions to work. It's possible to set up a CI pipeline that generates and commits it for you, but for the time being it's a lot simpler to just make sure this is committed. A workflow will soon come to make sure that the committed version is up-to-date with the source.

Comment thread secscan/src/index.ts
Comment on lines +27 to +31
export function getSnapInfoField(output: string, field: string): string {
return (
output.match(new RegExp(`^${field}:\\s+(.+)$`, "m"))?.[1]?.trim() ?? ""
);
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regex pulls out the value in the name: or version: field from snap info <snap_file>

Comment thread secscan/src/index.ts
}

export async function runScanner(
scanner: (typeof SUPPORTED_SCANNERS)[number],

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type-narrows scanner from "any string" to "only the strings contained by SUPPORTED_SCANNERS".

Comment thread secscan/src/index.ts
tokensDir: string,
resultsDir: string,
): Promise<void> {
const tokenFile = join(tokensDir, `${scanner}-token.txt`);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The secscan client writes session tokens to this file so subsequent commands can reference an in-progress or completed scan. It is no longer needed once the scan results are collected.

Comment thread secscan/src/index.ts
const reportExt = SCANNER_OUTPUT_FORMAT[scanner];
const reportFile = join(scannerResultsDir, `${scanner}_report.${reportExt}`);
await writeFile(reportFile, capturedOutput);
await resultPromise;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably me gilding the lily a bit, but I await resultPromise at the very end here just because its result is irrelevant to this method, so I didn't want to await in the middle of actual calculations

Comment thread secscan/src/index.ts
Comment on lines +231 to +233
if (import.meta.url === `file://${process.argv[1]}`) {
run();
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the TS equivalent to if __name__ == "__main__":, used to avoid invoking run() when importing the module for tests.

Comment thread build.mjs
Comment on lines +17 to +19
banner: {
js: "import { createRequire as __require__ } from 'module'; globalThis.require = __require__(import.meta.url);",
},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This prepends the string to dist/index.js for each built action. The string fixes an issue that's frankly really convoluted and circular and exemplifies the Node ecosystem. TL;DR, there's multiple "kinds" of Node scripts and modern TypeScript is not the same "kind" as the flattened JavaScript we need to output for GitHub Actions, and this is mostly fine but it caused some confusion around the require function and this is the workaround that the internet recommends.

Comment thread package.json
Comment on lines +7 to +9
"build": "pnpm -r run build",
"test": "pnpm -r run test",
"lint": "eslint . && tsc --noEmit"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each of these can be run at the root of the project as pnpm <script>. pnpm -r <action> will find all subdirectories that contain a package.json file and run pnpm <action>. You can also run it for a specific action without cding with, for example, pnpm lint -F secscan.

Actions here are overridden by actions of the same name in subdirectories.

Comment thread package.json
}
},
"type": "module",
"devDependencies": {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These dependencies are inherited by subdirectories and are common dependencies other actions may want. It's the compiler, the testing framework, and the linter.

Comment thread pnpm-workspace.yaml

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This says "everything with a package.json is part of this workspace, and esbuild is the only command that's allowed to be executed during builds" (we love not having ACE vulnerabilities in our build systems)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants