Security
What flowpact reads, writes and sends, the permissions it needs, how releases are built and signed, and how to verify them.
flowpact is a static analyzer: it reads the workflow, action, config and contract files of a repository and reports on them. This page is for the people who review it before it runs in their organization. To report a vulnerability, see the security policy.
The threat model goes further: it names what flowpact and its releases protect and where untrusted data and code cross into them, ranks the threats, and gives for each the code and tests that defend against it and the risk that remains.
What it reads and writes
- Reads the repository it analyzes:
.github/workflows/*.yml, local actions (action.yml), the config (.github/flowpact/flowpact.config.yml) and the contracts (.github/flowpact/contracts/). With impact mode, it also reads the same files at the pull request's base commit through git. - Writes only what you ask for: reports to stdout or
--output, contracts withflowpact generate, and a patch withflowpact check --patch. It never commits or pushes. - The GitHub Action additionally writes annotations, the job summary and its outputs, and, in
mode: checkwhen contracts drifted, uploads the regenerated contracts as a workflow artifact (upload-contracts).
flowpact treats the analyzed files as untrusted input:
- it never writes contracts, reports or patches through a symlink in the repository (or, in GitHub Actions, the
workspace), which a pull request could commit; it writes outside the repository only to paths you pass, such as
-o markdown:"$GITHUB_STEP_SUMMARY"; - it escapes control characters in every finding, and keeps its output from being read as
workflow commands (
::…at the start of a line,##[…]anywhere), except the annotations it prints on purpose with--format github; - it reads the value of a flag only as that flag:
--title "$PR_TITLE"cannot add a--pluginor an-o, turn off--no-plugins, or end the run as--helpwould; - it caps matrix expansion and YAML alias expansion.
Reports of a way around any of this are in scope of the security policy.
Network and telemetry
flowpact has no telemetry and sends nothing anywhere.
- The CLI, the language server and the VS Code extension make no network requests.
- The GitHub Action makes two kinds, both to GitHub and both optional:
- in impact mode, when the checkout is shallow, a
git fetchof the base commit (and, for release pull requests, the release tags) from the repository's ownorigin, with thetokeninput as a one-off authorization header; - in
mode: checkwithupload-contracts, the upload of the contract drift artifact, made the wayactions/upload-artifactmakes it: to the runner's artifact service, and the zip itself to the storage address that service signs for it. It uses the runner's own token for the artifact service, not thetokeninput.
- in impact mode, when the checkout is shallow, a
On GitHub Enterprise Server, the git fetch goes to the enterprise server, never to github.com. The artifact upload is
not available there: the action warns that it could not upload the artifact and still writes the patch (the patch
output). Set upload-contracts: false there.
Permissions
- CLI and editor: none beyond reading the repository. They run with the permissions of whoever runs them.
- GitHub Action:
contents: readto check out the repository; the action itself does not call the GitHub API. Uploading SARIF to code scanning is a separate step (github/codeql-action/upload-sarif) that needssecurity-events: write. See action permissions.
Plugins run code
Plugins are JavaScript modules from the analyzed repository, listed in its config. They run with the permissions and environment of whatever runs flowpact. flowpact never downloads them.
- The GitHub Action's
plugins: auto(the default) skips them onpull_request_targetandworkflow_run, where the repository's code may come from an untrusted fork while the job holds secrets. Plugins named in itsplugininput, such as an organization's rules in its copy of the action, still load, except a file in the checkout: that one is skipped like the config's plugins. - The VS Code extension loads them only in a trusted workspace, and only for repositories inside the workspace folders.
- The CLI loads the plugins the config lists unless you pass
--no-plugins; pass it on code you do not trust. Plugins given with--plugin, such as the rules of a wrapper, still load.
If you do not use plugins, set plugins: false in the action.
How releases are built
- Built only by GitHub Actions, from a tag, in this repository's
release workflow. Release tags (
v*) can only be created or moved by the release automation and maintainers. Every workflow pins its actions to commit SHAs. - npm: published with npm trusted publishing (no long-lived token) and npm provenance.
- GitHub release assets: the npm tarball, the VS Code extension (
.vsix) and the GitHub Action as an archive (flowpact-action-X.Y.Z.tar.gz:action.yml, the bundle, its license notices and SBOM). Each is signed with a Sigstore bundle (.sigstore.json) and carries in-toto build provenance (.intoto.jsonl). - What each artifact contains: every bundle ships
THIRD_PARTY_LICENSES.txt(each bundled package with its license text) andsbom.cdx.json(a CycloneDX SBOM). Each SBOM is also attested against its artifact (a CycloneDX attestation whose subject is the artifact), and the release attaches a copy (flowpact-X.Y.Z.cdx.json,flowpact-action-X.Y.Z.cdx.json,flowpact-vscode-X.Y.Z.cdx.json). flowpact's workflow model,@ciatlas/core, is built from its own repository's release tag rather than npm, and the SBOM names it so:pkg:github/ciatlas/core@vX.Y.Z. - Published only once complete: a release stays a draft until every signed asset and SBOM is attached, and a published release is never changed.
- Immutable: from 0.10.0, every release is a GitHub immutable release: once published, its tag and files cannot change, and GitHub signs a release attestation of the tag, the commit and every file. Drafting first and publishing only once every file is attached is the flow GitHub recommends for it.
- Reproducible: the action's bundle is committed, and CI fails when building it from source gives different bytes.
The action archive and the
.vsixare built deterministically from the tag.
The action archive, the SBOMs and draft releases start with 0.9.0.
Verify a release
Download the assets of a release (gh release download vX.Y.Z --repo ciatlas/flowpact), then run the commands below.
Releases up to 0.11.1 were built before the move, and GitHub keeps their attestations under the old owner: verify
them online with --owner rumankazi, or offline against the bundle attached to the release with
gh attestation verify <file> --bundle <file>.sigstore.json --repo rumankazi/flowpact. gh release verify no longer
finds their release attestations.
# Built by this repository's release workflow:
gh attestation verify flowpact-X.Y.Z.tgz --repo ciatlas/flowpact
gh attestation verify flowpact-action-X.Y.Z.tar.gz --repo ciatlas/flowpact
gh attestation verify flowpact-vscode-X.Y.Z.vsix --repo ciatlas/flowpact
# An SBOM was attested for that artifact, and the attached copy is the attested one:
gh attestation verify flowpact-X.Y.Z.tgz --repo ciatlas/flowpact --predicate-type https://cyclonedx.org/bom \
--format json --jq '.[0].verificationResult.statement.predicate' | jq -S . > attested.cdx.json
jq -S . flowpact-X.Y.Z.cdx.json | diff - attested.cdx.json
# The npm package you installed is the one that was published with provenance:
npm audit signatures
# From 0.10.0: the release is immutable, and each file is the one GitHub attested for it:
gh release verify vX.Y.Z --repo ciatlas/flowpact
gh release verify-asset vX.Y.Z flowpact-X.Y.Z.tgz --repo ciatlas/flowpactTo check that the GitHub Action at a tag is the released one, compare the archive's files with the tag:
mkdir action && tar -xzf flowpact-action-X.Y.Z.tar.gz -C action
git clone --depth 1 --branch vX.Y.Z https://github.com/ciatlas/flowpact.git source
diff -r action/packages/action/dist source/packages/action/dist && cmp action/action.yml source/action.ymlPin the action to the commit you verified: uses: ciatlas/flowpact@<commit-sha> # vX.Y.Z.
Repository practices
- Changes reach
mainonly through squash-merged pull requests that pass the required checks: CI passed, which includes flowpact's lint of its own workflows, and Conventional PR title. Nobody can push tomaindirectly, force-push it or delete it, and no one is exempt from the checks (themainruleset has no bypass). A pull request also needs a code owner's approval, except that the maintainer, the only repository admin, may merge any pull request without an approving review (GitHub does not let authors approve their own), and Renovate, which merges only its own pull requests (non-major updates, once the checks pass). The rules are kept in.github/rulesets/, and a weekly run reverts changes made in the settings UI. MAINTAINERS.md lists who has access to what. - CodeQL (extended queries), secret scanning with push protection, dependency updates (Renovate) and the OpenSSF Scorecard run on the repository.
- Vulnerabilities are reported privately, and only the latest release receives fixes; see the security policy, which also lists the security fixes in past releases.
- flowpact meets the OpenSSF Best Practices criteria at the passing
level;
.bestpractices.jsonin the repository records each answer and its evidence.
Comparison
What flowpact reports that actionlint and GitHub's VS Code extension do not, with actionlint's and flowpact's output side by side, and where the tools overlap.
Threat model
What flowpact protects, where untrusted data and code cross into it, and its threats ranked by risk, each with the code and tests that defend against it and the risk that remains.