flowpact

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 with flowpact generate, and a patch with flowpact check --patch. It never commits or pushes.
  • The GitHub Action additionally writes annotations, the job summary and its outputs, and, in mode: check when 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 --plugin or an -o, turn off --no-plugins, or end the run as --help would;
  • 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 fetch of the base commit (and, for release pull requests, the release tags) from the repository's own origin, with the token input as a one-off authorization header;
    • in mode: check with upload-contracts, the upload of the contract drift artifact, made the way actions/upload-artifact makes 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 the token input.

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: read to 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 needs security-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 on pull_request_target and workflow_run, where the repository's code may come from an untrusted fork while the job holds secrets. Plugins named in its plugin input, 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) and sbom.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 .vsix are 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/flowpact

To 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.yml

Pin the action to the commit you verified: uses: ciatlas/flowpact@<commit-sha> # vX.Y.Z.

Repository practices

  • Changes reach main only 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 to main directly, force-push it or delete it, and no one is exempt from the checks (the main ruleset 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.json in the repository records each answer and its evidence.

On this page