pinact

repository·main·Indexed 22 days ago

https://github.com/suzuki-shunsuke/pinact

A CLI tool for securing GitHub workflows by pinning GitHub Actions and Reusable Workflows to specific commit SHAs instead of mutable tags or branches. It provides features for updating pins to the latest versions, enforcing a minimum release age (cooldown period) to mitigate supply chain risks, generating SARIF output for code scanning, and supporting GitHub Enterprise Server (GHES) instances.

Tokens
12.6K
Snippets
55
Records
71
Agent score
75%

What's inside pinact

  1. Understand GHES action search and fallback behavior

    main

    When GHES is enabled, pinact changes how it searches for GitHub Actions:

    1. If fallback: false (Default): pinact searches only the GHES instance. If an action is not found on GHES, an error is returned. This is the most secure mode and aligns with GHES defaults where GitHub Connect is disabled.
    2. If fallback: true: pinact first searches the GHES instance. If the GHES API returns a 404 Not Found for the repository, pinact then searches github.com. Results are cached per repository to optimize performance.

    Important Constraints:

    • No Fallback for Comments: While action searching can fallback to github.com, the creation of PR review comments (via pinact run -review) does not fallback. If GHES is enabled, pinact attempts to post the comment to the GHES instance. If this fails, an error is returned.
    • Single Instance: Only one GHES instance is supported at a time.
    • Error Handling: If a GHES API request fails with any error other than a 404, pinact will return that error immediately and will not attempt to fallback to github.com.
  2. Understand pinact configuration merging (Global vs Project)

    main

    pinact supports a global configuration file for user-wide defaults which is merged with project-level configuration files (like .pinact.yaml).

    Global Configuration Paths

    1. $PINACT_GLOBAL_CONFIG (if set)
    2. Linux/macOS: $XDG_CONFIG_HOME/pinact/pinact.yaml or ~/.config/pinact/pinact.yaml
    3. Windows: %APPDATA%\pinact\pinact.yaml

    Merge Behavior

    When both exist, they are merged field-by-field. Project values generally take precedence.

    FieldMerge Behavior
    versionProject if non-zero, otherwise global
    filesProject if non-empty, otherwise global
    separatorProject if non-empty, otherwise global
    ghesProject if set (whole-object replace), otherwise global
    min_age.valueProject if set, otherwise global
    min_age.alwaysProject if set, otherwise global
    ignore_actionsConcatenated: [global..., project...]
    rulesConcatenated: [global..., project...] (later rules win per-field)

    Note: Each file's schema version is validated independently. If either file uses an unsupported version, pinact will abort.

  3. How pinact handles non-semver versions

    main

    To ensure safety and avoid unexpected workflow changes, pinact follows these rules regarding version pinning:

    1. Semver versions: If a version follows semantic versioning (e.g., v1), pinact will update it to a newer semver (e.g., v1.1.0) only if the SHA remains identical. If the SHA changes, pinact will not automatically update the version to a different semver.
    2. Non-semver versions: Versions that do not follow semver (e.g., main, master, release/v1) are treated as potential branches. pinact does not pin these to a full commit SHA because doing so would prevent future semantic version updates and could break workflows if the branch is unstable.
    3. Safety Design: pinact is designed to pin actions without changing the underlying SHA at the moment of pinning, unless it is a safe semver update. It avoids pinning branches to SHAs because branch SHAs are volatile.
  4. Understand the motivation for pinning by commit SHA

    main

    The primary goal of pinact is to encourage pinning GitHub Actions versions by their full-length commit SHA rather than tags or semver.

    Why commit SHAs?

    • Immutability: GitHub tags are mutable, which poses security and reliability risks. Pinning to a full-length SHA is the only way to ensure an action is used as an immutable release.
    • Security: Pinning to a specific SHA mitigates the risk of a bad actor adding a backdoor, as they would need to generate a SHA-1 collision to bypass the check.

    Comparison:

    • Good: uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8
    • Bad (Tag): uses: actions/cache@v3.3.1
    • Bad (Semver): uses: actions/cache@v3
    # Recommended approach
    uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 # v3.3.1
  5. How pinact determines release age

    main

    When using --min-age, pinact calculates the age of a version using the following logic depending on the target type:

    • GitHub Releases: Uses the PublishedAt field from the GitHub API.
    • Tags: Uses the Committer.Date from the commit object (via GET /repos/{owner}/{repo}/git/commits/{sha}).

    Note: If the commit for a tag cannot be fetched, the tag is skipped and a warning is logged.

  6. Configure minimum release age (cooldown) with `-min-age`

    main

    To mitigate supply chain risks, you can enforce a "cooldown" period for new action releases. This prevents pinact from automatically adopting very recent releases that haven't been vetted by the community.

    Supported methods for setting minimum age:

    1. CLI Flag: -min-age <days>
    2. Environment Variable: PINACT_MIN_AGE
    3. Configuration File: Using .pinact.yml with min_age.value or rule-specific min_age.

    Verification Modes:

    • During Update: When using -update, pinact filters out versions that don't meet the age requirement. If no suitable version is found, pinact exits with an error.
    • Verification Only: Use -verify-min-age to check if your currently pinned versions meet the minimum age requirement without attempting to update them.
    # Example .pinact.yml configuration
    min_age:
      value: 7
      always: true # Verifies current versions every run
    
    rules:
      - min_age: 0
        conditions:
          - expr: | 
              ActionRepoOwner == "suzuki-shunsuke"
  7. How GHES fallback to github.com works

    main

    By default, when GHES support is enabled, pinact searches for actions only on the GHES instance.

    If fallback is set to true (via ghes.fallback in config or PINACT_GHES_FALLBACK in env), pinact will first search the GHES instance. If a repository is not found (returns a 404), pinact will then search github.com. This behavior is recommended when GitHub Connect is enabled.

  8. Configure YAML Language Server for pinact schemas

    main

    To enable autocompletion and validation in your IDE using the YAML Language Server, add the appropriate $schema comment to the top of your .pinact.yaml file.

    For the latest version:

    # yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/pinact/main/json-schema/pinact.json

    For a specific pinned version (e.g., v1.1.2):

    # yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/pinact/v1.1.2/json-schema/pinact.json
  9. Initialize a pinact configuration file

    main

    Use pinact init to create a configuration file. By default, it creates .pinact.yaml in the current directory. You can specify an explicit path or create a user-wide global configuration.

    • Local config: pinact init (creates .pinact.yaml in the current directory)
    • Explicit path: pinact init <path>
    • Global config: pinact init -g (creates ~/.config/pinact/pinact.yaml on Unix or %APPDATA%\pinact\pinact.yaml on Windows)
    $ pinact init                          # creates .pinact.yaml in the current directory
    $ pinact init .github/pinact.yaml      # explicit path
    $ pinact init -g                       # creates the user-wide global config
  10. Verify pinact assets from GitHub Releases

    main

    1. GitHub CLI

    Install GitHub CLI via aqua, then use gh attestation verify to check the asset against the release workflow.

    aqua g -i cli/cli
    
    version=v1.0.0
    asset=pinact_darwin_arm64.tar.gz
    gh release download -R suzuki-shunsuke/pinact "$version" -p "$asset"
    gh attestation verify "$asset" \
      -R suzuki-shunsuke/pinact \
      --signer-workflow suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml

    2. slsa-verifier

    Install slsa-verifier via aqua, then use verify-artifact with the downloaded provenance file.

    aqua g -i slsa-framework/slsa-verifier
    
    version=v1.0.0
    asset=pinact_darwin_arm64.tar.gz
    gh release download -R suzuki-shunsuke/pinact "$version" -p "$asset" -p multiple.intoto.jsonl
    slsa-verifier verify-artifact "$asset" \
      --provenance-path multiple.intoto.jsonl \
      --source-uri github.com/suzuki-shunsuke/pinact \
      --source-tag "$version"

    3. Cosign

    Install cosign via aqua. This method verifies the signature, certificate, and identity, then validates the checksum.

    aqua g -i sigstore/cosign
    
    version=v1.0.0
    checksum_file="pinact_${version#v}_checksums.txt"
    asset=pinact_darwin_arm64.tar.gz
    gh release download "$version" \
      -R suzuki-shunsuke/pinact \
      -p "$asset" \
      -p "$checksum_file" \
      -p "${checksum_file}.pem" \
      -p "${checksum_file}.sig"
    cosign verify-blob \
      --signature "${checksum_file}.sig" \
      --certificate "${checksum_file}.pem" \
      --certificate-identity-regexp 'https://github\.com/suzuki-shunsuke/go-release-workflow/\.github/workflows/release\.yaml@.*' \
      --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
      "$checksum_file"
    cat "$checksum_file" | sha256sum -c --ignore-missing
  11. Upgrade from v3 to v4: Handling the removal of `-review`

    main

    In version 4, the --review, --repo-owner, --repo-name, --pr, and --sha options have been removed. To achieve review functionality, you must now output results in SARIF format and pipe them to reviewdog.

    pinact run -format sarif |
      reviewdog -f sarif -name pinact -reporter github-pr-review
  12. Generate SARIF output for code scanning

    main

    pinact can output results in SARIF format, which is compatible with GitHub SARIF Code Scanning and tools like reviewdog.

    Note: Using --format sarif implies -fix=false (validation only). If you want to generate SARIF reports for changes that pinact actually applies, you must explicitly include the -fix flag.

    # Generate SARIF for GitHub Code Scanning
    pinact run --format sarif -fix > sarif.json
    
    # Pipe SARIF to reviewdog for PR comments
    pinact run -format sarif | reviewdog -f sarif -name pinact -reporter github-pr-review