poutine looks for the mistakes in your CI workflow definitions
poutine, a supply chain vulnerability scanner for build pipelines
At a glance
- What is it?
- poutine is a Go scanner that parses GitHub Actions, GitLab CI, Azure DevOps and Tekton pipeline files and reports the misconfigurations that let an attacker into your build, such as code execution from untrusted changes. It reads the workflow as data, judges it with Open Policy Agent rules, and outputs SARIF so it fits into the code scanning you already have.
- Who is it for?
- Adopt poutine when you own CI pipeline definitions and want them checked for the misconfigurations that lead to build-time compromise, and when you can add it as a check that fails on findings. Do not adopt it as a scanner for your dependencies or your code, since it analyses workflow files rather than what a workflow fetches at run time.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 9 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What it analyses and what it does not
poutine is described as a security scanner that detects misconfigurations and vulnerabilities in the build pipelines of a repository, created by BoostSecurity.io. It supports parsing CI workflows from GitHub Actions and Gitlab CI/CD, and the supported platforms list adds Azure DevOps and Pipelines As Code Tekton.
The unit of analysis is the pipeline definition, not the software it builds. That is the key thing to understand before you use it, and it is also why the name is apt. The README's example screenshot is a finding about arbitrary code execution from untrusted code changes, which is the classic CI attack: a workflow triggers on a pull request from a fork, checks out the untrusted code, and runs a build or test step that executes it, at which point the attacker has your runner and your secrets.
So poutine reads the workflow file, understands how the trigger, the permissions, the checkout and the steps relate, and applies rules to that structure. The rules are documented in the docs directory under the rules content, and the list of rules currently supported is what you should read first, because a scanner with rules you have not looked at is a scanner you cannot calibrate.
The project also does something beyond single repositories. When given an access token with read-level access, poutine can analyze all the repositories of an organization to quickly gain insights into the security posture of the organization's software supply chain.
That organisation-wide mode is the reason the tool exists in a security platform context rather than as a lint rule. A single repository's workflow can be reviewed by a human; a hundred repositories cannot, and the interesting question for an organisation is whether any of them has a workflow that a fork can trigger into a privileged context.
The name comes from Quebec, and the README explains it: in French the word can mean messy, and inspired by the complexity and intertwined dependencies of modern open-source projects it reflects both the Montreal roots and the messy nature of securing software supply chains.
The project is Go, Apache 2.0 licensed, with the last push on 2026-10-14 and the tags v1.1.6 from 2026-05-22, v1.1.5 from the same day and v1.1.4 from 2026-04-17.
Four ways to install it
The install options cover the ways a scanner like this actually gets deployed.
The primary route is to download the latest release from the releases page and add the binary to your PATH. There is a Homebrew formula:
brew install poutineAnd a container image:
docker run -e GH_TOKEN ghcr.io/boostsecurityio/poutine:latestThe token in that command is the point. Even for a local run against a public repository, the scanner wants a token, and for the organisation-wide command it is required.
The route that matters most for CI is the GitHub Action, and the workflow it ships is short and security-conscious:
jobs:
poutine:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
#################################################################################################
- name: poutine - GitHub Actions SAST
uses: boostsecurityio/poutine-action@main # We recommend to use a tagged version and pin it
#################################################################################################
- name: Upload poutine SARIF file
uses: github/codeql-action/upload-sarif@4355270be187e1b672a7a1c7c7bae5afdc1ab94a # v3.24.10
with:
sarif_file: results.sarifThree details in that are worth copying. The permissions block is minimal: `security-events: write` so the results can be uploaded as code scanning alerts, and `contents: read` so the checkout works, and nothing else. The action is pinned to a commit SHA with the version in a comment, and the comment on the poutine action itself says a tagged version should be used and pinned.
The results are uploaded through the CodeQL action's SARIF uploader, so findings land in GitHub's code scanning tab alongside the results from other static analysis tools rather than in a separate system nobody looks at.
There is also a pre-commit hook, which puts the scan in front of the developer rather than in the pipeline:
repos:
- repo: https://github.com/boostsecurityio/poutine
rev: v1.1.4 # Use the latest version
hooks:
- id: poutineThe commands and what each one is for
The command surface is three commands, and the difference between them is scope.
For a repository you already have on disk:
poutine analyze_local .For a single repository on GitHub:
poutine analyze_repo org/repo --token "$GH_TOKEN"For a whole GitHub organization:
poutine analyze_org org --token "$GH_TOKEN"And for a self-hosted GitLab instance, with the platform and the base URL given explicitly:
poutine analyze_org my-org/project --token "$GL_TOKEN" --scm gitlab --scm-base-url https://gitlab.example.comThe local command needs no token, which makes it the one you would wire into a pre-commit hook or a developer laptop. The token is only required for the remote commands, and the README specifies read-level access for it, which is the right scope for a scanner that reads workflow files.
The organisation command is the interesting one from a scale perspective. A token with read access to every repository in an organisation is a broad grant, and the option list has `--ignore-forks`, which ignores forked repositories in the organisation. That is a sensible default to think about, because a fork is a copy of your workflow files and would otherwise produce duplicate findings for code you do not control.
The self-hosted GitLab form shows the same tool working against a different SCM rather than being GitHub-specific, with `--scm` selecting the platform, defaulting to github, and `--scm-base-url` giving the base URI of the instance.
Options, output formats and the version check
The full option list is in the README, and a few entries matter more than the others.
--token SCM access token (required for the commands analyze_repo, analyze_org) (env: GH_TOKEN)
--format Output format (default: pretty, json, sarif)
--ignore-forks Ignore forked repositories in the organization (analyze_org)
--scm SCM platform (default: github, gitlab)
--scm-base-url Base URI of the self-hosted SCM instance
--threads Number of threads to use (default: 2)
--config Path to the configuration file (default: .poutine.yml in the working directory, or .github/poutine.yml)
--skip Add rules to the skip list for the current run (can be specified multiple times)
--verbose Enable debug logging
--fail-on-violation Exit with a non-zero code (10) when violations are found
--disable-version-check Disable the once-per-day check for newer poutine releases (env: POUTINE_DISABLE_VERSION_CHECK, config: disableVersionCheck)The three output formats are the point of the `--format` option. `pretty` is for a human reading a terminal, `json` is for a machine, and `sarif` is for a code scanning product, which is what the GitHub Action in the README uses.
`--fail-on-violation` is what turns the scanner into a gate. The exit code is 10, a distinct value rather than 1, so a wrapper script can tell a findings failure from a crash.
The `--config` discovery order is worth noting: `.poutine.yml` in the working directory, or `.github/poutine.yml`, and a file at the repository root wins when both exist. That mirrors where GitHub already keeps its own metadata.
`--skip` adds rules to the skip list for the current run and can be repeated. It is the escape hatch every scanner needs, and also the easiest way to end up with a permanently quiet scanner.
The version check is the one item that deserves a careful read, because it is telemetry. By default poutine reaches out at most once every 24 hours to check whether a newer release is available. The request reports the current poutine version, an anonymous instance identifier persisted in `~/.poutine/config.yaml`, and a count of CLI invocations since the last check. No source, repository or finding data is sent.
That is a narrow disclosure, and the README says exactly what is sent, which is the right way to do it. It can be disabled three ways: the `--disable-version-check` flag, the `POUTINE_DISABLE_VERSION_CHECK=1` environment variable, or `disableVersionCheck: true` in the configuration file.
Custom rules are Rego
The extensibility mechanism is OPA's Rego policy language, and that choice explains a lot about the tool.
poutine supports custom Rego rules to extend its security scanning capabilities, and you write your own rules and include them at run time. The configuration file points at the directories holding them:
include:
- path: ./custom_rules
- path: ./github_actionsThe rules have four requirements: they must be saved as `.rego` files in the included directory, they must follow the package naming convention `package rules.<rule_name>`, they must define a `rule` variable with metadata, and they must define a `results` set containing the findings.
The example rule shows the shape:
package rules.custom_injection
import data.poutine
import rego.v1
# METADATA
# title: Custom Injection Detection
# description: Detects potential injection vulnerabilities in workflows
# custom:
# level: warning
rule := poThe metadata block is the OPA convention for annotating a rule with a title, a description and, here, a custom `level` of warning. The `import data.poutine` line is how a custom rule reads the parsed pipeline data that the scanner has already normalised, and `import rego.v1` selects the current Rego syntax.
Two things follow from using Rego rather than a bespoke DSL. The rules are a policy language with its own documentation and its own tooling, so a team that already writes OPA policies can write scanner rules without learning anything new. And the rules are data, which means they can be reviewed, diffed and shared the way any other configuration is, and the separation between the scanner's parsing and the judgement about what is wrong stays clean.
The cost is that Rego is not a scripting language, and writing a rule means learning its syntax. The scanner's own rule set is the reference implementation to read.
How it is built, and the limits of a definition scanner
The dependency list in `go.mod` describes the implementation, and the interesting entries are the policy engine and the format libraries.
`github.com/open-policy-agent/opa` at v1.15.1 is the policy engine that evaluates the rules, and `github.com/owenrumney/go-sarif/v2` is the SARIF writer that produces the output you upload to code scanning. `github.com/spf13/cobra` and `viper` provide the command line and the configuration handling, `github.com/google/go-github/v84` and `gitlab.com/gitlab-org/api/client-go` are the two SCM clients, and `github.com/rs/zerolog` and `github.com/schollz/progressbar/v3` do the logging and the progress display.
Three dependencies point at features beyond the core scanner. `github.com/mark3labs/mcp-go` is the Model Context Protocol library, and the repository has an `MCP_INTEGRATION.md`, so the scanner can be driven by an agent as well as from the command line. `github.com/package-url/packageurl-go` handles package URLs, which is how the scanner names the actions and dependencies it finds. And `github.com/gkampitakis/go-snaps` is a golden file testing library, so the scanner's own tests assert on stored outputs.
The repository layout is organised around that structure, with `scanner/` for the analysis, `providers/` for the SCM and platform integrations, `opa/` for the policy layer, `formatters/` for the output formats, `results/` for the finding model, `versioncheck/` for the release check, and `analyze/`, `cmd/`, `models/`, `bench/` and `tools/` around them. There is a `dagger.json` and a `dagger/` directory, so the project's own CI runs on Dagger.
The limitation is inherent to the approach. poutine reasons about the pipeline definition, so it can tell you that a workflow checks out untrusted code in a privileged context, and it can tell you that an action is referenced by a mutable tag rather than a pinned SHA. What it cannot tell you is whether the action behind that tag is malicious, or whether a checkout step is safe in a workflow it cannot parse.
In other words, the findings are patterns with known consequences, and the judgement about how serious a given pattern is in your environment stays with you. That is why `--skip` exists, why the rule documentation matters, and why the tool fits alongside code scanning rather than replacing it.
Editorial conclusion
Adopt poutine when you own CI pipeline definitions and want them checked for the misconfigurations that lead to build-time compromise, and when you can add it as a check that fails on findings. Do not adopt it as a scanner for your dependencies or your code, since it analyses workflow files rather than what a workflow fetches at run time. Verify first that your platform is one of the four supported, that the token you give it has read-level access only, that you have decided what to do about the version check telemetry, since it is enabled by default, and that the rules you skip with `--skip` are genuinely safe, because a skipped rule is a finding you have agreed not to see.
Frequently asked questions
What is poutine?
It is a security scanner created by BoostSecurity.io that detects misconfigurations and vulnerabilities in the build pipelines of a repository. It parses CI workflows from GitHub Actions, Gitlab CI/CD, Azure DevOps and Pipelines As Code Tekton, and with a read-level access token it can analyse all the repositories of an organisation.
How do I install and run poutine?
Download the binary from the releases page, install it with `brew install poutine`, run the container image with `docker run -e GH_TOKEN ghcr.io/boostsecurityio/poutine:latest`, or use the GitHub Action. For a local repository run `poutine analyze_local .`, for one remote repository `poutine analyze_repo org/repo --token "$GH_TOKEN"`, and for an organisation `poutine analyze_org org --token "$GH_TOKEN"`.
What output formats does poutine support?
Three, selected with `--format`: pretty for a terminal, json for machine consumption and sarif for code scanning products. The GitHub Action in the README uploads the SARIF file with the CodeQL upload-sarif action, so findings appear in GitHub code scanning.
Does poutine send any data to its developers?
By default it reaches out at most once every 24 hours to check for a newer release, reporting the current poutine version, an anonymous instance identifier stored in `~/.poutine/config.yaml` and a count of CLI invocations since the last check. No source, repository or finding data is sent, and the check can be disabled with the flag, the `POUTINE_DISABLE_VERSION_CHECK` environment variable or the `disableVersionCheck` config option.
Can I write my own poutine rules?
Yes, in OPA Rego. Rules must be `.rego` files in an included directory, follow the `package rules.<rule_name>` naming convention, define a `rule` variable with metadata and a `results` set with findings. They are included from a `.poutine.yml` file through the `include` directive with a `path` per directory.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/boostsecurityio-poutine)Was this useful?