CLI tool
boostsecurityio/poutine avatar
boostsecurityio/poutine

poutine looks for the mistakes in your CI workflow definitions

poutine, a supply chain vulnerability scanner for build pipelines

526 stars43 forksGoApache-2.0

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:

bash
brew install poutine

And a container image:

bash
docker run -e GH_TOKEN ghcr.io/boostsecurityio/poutine:latest

The 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:

yaml
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.sarif

Three 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:

yaml
repos:
  - repo: https://github.com/boostsecurityio/poutine
    rev: v1.1.4  # Use the latest version
    hooks:
      - id: poutine

The 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:

bash
poutine analyze_local .

For a single repository on GitHub:

bash
poutine analyze_repo org/repo --token "$GH_TOKEN"

For a whole GitHub organization:

bash
poutine analyze_org org --token "$GH_TOKEN"

And for a self-hosted GitLab instance, with the platform and the base URL given explicitly:

bash
poutine analyze_org my-org/project --token "$GL_TOKEN" --scm gitlab --scm-base-url https://gitlab.example.com

The 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.

code
--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:

yaml
include:
  - path: ./custom_rules
  - path: ./github_actions

The 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:

rego
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 := po

The 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

  1. boostsecurityio/poutine on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/boostsecurityio-poutine.svg)](https://hysenlabs.com/projects/boostsecurityio-poutine)

Was this useful?