[
  {
    "path": ".github/FUNDING.yml",
    "content": "# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository\ngithub:\n  - suzuki-shunsuke\n"
  },
  {
    "path": ".github/workflows/actionlint.yaml",
    "content": "---\nname: actionlint\non: pull_request\njobs:\n  actionlint:\n    runs-on: ubuntu-24.04\n    timeout-minutes: 10\n    permissions:\n      contents: read\n      pull-requests: write\n    steps:\n      - uses: suzuki-shunsuke/actionlint-action@8297c48141939cdcf80a8341c3cd525b300e36db # v0.1.2\n"
  },
  {
    "path": ".github/workflows/autofix.yaml",
    "content": "---\nname: autofix.ci\non: pull_request\npermissions: {}\njobs:\n  autofix:\n    runs-on: ubuntu-24.04\n    permissions: {}\n    timeout-minutes: 15\n    steps:\n      - uses: suzuki-shunsuke/go-autofix-action@ba716cebb7767055bdde0e62bb91d715bde39ab2 # v0.1.12\n        with:\n          aqua_version: v2.59.0\n"
  },
  {
    "path": ".github/workflows/check-commit-signing.yaml",
    "content": "---\nname: Check if all commits are signed\non:\n  pull_request_target:\n    branches: [main]\nconcurrency:\n  group: ${{ github.workflow }}--${{ github.head_ref }} # github.ref is unavailable in case of pull_request_target\n  cancel-in-progress: true\njobs:\n  check-commit-signing:\n    uses: suzuki-shunsuke/check-commit-signing-workflow/.github/workflows/check.yaml@547eee345f56310a656f271ec5eaa900af46b0fb # v0.1.0\n    permissions:\n      contents: read\n      pull-requests: write\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "---\nname: Release\non:\n  push:\n    tags: [v*]\njobs:\n  release:\n    uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@b2ecf54e35aca9e9689e761f5bd6d1ad9542a8cf # v8.0.0\n    with:\n      aqua_version: v2.59.0\n      go-version-file: go.mod\n    permissions:\n      contents: write\n      id-token: write\n      actions: read\n      attestations: write\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "---\nname: test\non: pull_request\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\npermissions: {}\njobs:\n  test:\n    uses: ./.github/workflows/workflow_call_test.yaml\n    permissions:\n      pull-requests: write\n      contents: read\n  status-check:\n    runs-on: ubuntu-24.04\n    if: always() && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled'))\n    timeout-minutes: 10\n    permissions: {}\n    needs:\n      - test\n    steps:\n      - run: exit 1\n"
  },
  {
    "path": ".github/workflows/workflow_call_test.yaml",
    "content": "---\nname: test (workflow_call)\non: workflow_call\npermissions: {}\njobs:\n  test:\n    uses: suzuki-shunsuke/go-test-full-workflow/.github/workflows/test.yaml@e442a17816baa8a940edc1544cc6e739d870e165 # v5.0.2\n    with:\n      aqua_version: v2.59.0\n      golangci-lint-timeout: 120s\n    permissions:\n      pull-requests: write\n      contents: read\n"
  },
  {
    "path": ".gitignore",
    "content": "dist\n.coverage\nthird_party_licenses\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nlinters:\n  default: all\n  disable:\n    - depguard\n    - err113\n    - exhaustruct\n    - godot\n    - ireturn\n    - lll\n    - musttag\n    - nlreturn\n    - tagalign\n    - tagliatelle\n    - varnamelen\n    - wsl\n    - wsl_v5\n    - noinlineerr\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n    rules:\n      - path: _test\\.go\n        linters:\n          - goconst\nformatters:\n  enable:\n    - gci\n    - gofmt\n    - gofumpt\n    - goimports\n  exclusions:\n    generated: lax\n    paths:\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\nproject_name: ghalint\n\narchives:\n  - format_overrides:\n      - goos: windows\n        formats: [zip]\n    files:\n      - LICENSE\n      - README.md\n      - third_party_licenses/**/*\n\nenv:\n  - GO111MODULE=on\n\nbefore:\n  hooks:\n    - go mod tidy\n\nsboms:\n  - id: default\n    disable: false\n\nbuilds:\n  - main: ./cmd/ghalint\n    binary: ghalint\n    env:\n      - CGO_ENABLED=0\n    goos:\n      - windows\n      - darwin\n      - linux\n    goarch:\n      - amd64\n      - arm64\n\nrelease:\n  prerelease: \"true\" # we update release note manually before releasing\n  header: |\n    [Pull Requests](https://github.com/suzuki-shunsuke/ghalint/pulls?q=is%3Apr+milestone%3A{{.Tag}}) | [Issues](https://github.com/suzuki-shunsuke/ghalint/issues?q=is%3Aissue+milestone%3A{{.Tag}}) | https://github.com/suzuki-shunsuke/ghalint/compare/{{.PreviousTag}}...{{.Tag}}\n\nhomebrew_casks:\n  -\n    # NOTE: make sure the url_template, the token and given repo (github or gitlab) owner and name are from the\n    # same kind. We will probably unify this in the next major version like it is done with scoop.\n\n    repository:\n      owner: suzuki-shunsuke\n      name: homebrew-ghalint\n    # The project name and current git tag are used in the format string.\n    commit_msg_template: \"Brew formula update for {{ .ProjectName }} version {{ .Tag }}\"\n    homepage: https://github.com/suzuki-shunsuke/ghalint\n\n    description: GitHub Actions linter\n    license: MIT\n    skip_upload: true\n    hooks:\n      post:\n        install: |\n          if OS.mac?\n            system_command \"/usr/bin/xattr\", args: [\"-dr\", \"com.apple.quarantine\", \"#{staged_path}/ghalint\"]\n          end\n\nscoops:\n  - description: GitHub Actions linter for security best practices.\n    license: MIT\n    skip_upload: true\n    repository:\n      owner: suzuki-shunsuke\n      name: scoop-bucket\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nPlease read the following document.\n\n- https://github.com/suzuki-shunsuke/oss-contribution-guide\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Shunsuke Suzuki\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ghalint\n\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/suzuki-shunsuke/ghalint)\n[Install](docs/install.md) | [Policies](#policies) | [How to use](#how-to-use) | [Configuration](#configuration)\n\nGitHub Actions linter for security best practices.\n\n```console\n$ ghalint run\nERRO[0000] read a workflow file                          error=\"parse a workflow file as YAML: yaml: line 10: could not find expected ':'\" program=ghalint version= workflow_file_path=.github/workflows/release.yaml\nERRO[0000] github.token should not be set to workflow's env  env_name=GITHUB_TOKEN policy_name=workflow_secrets program=ghalint version= workflow_file_path=.github/workflows/test.yaml\nERRO[0000] secret should not be set to workflow's env    env_name=DATADOG_API_KEY policy_name=workflow_secrets program=ghalint version= workflow_file_path=.github/workflows/test.yaml\n```\n\nghalint is a command line tool to check GitHub Actions Workflows and action.yaml for security policy compliance.\n\n## :bulb: We've ported ghalint to lintnet module\n\n- https://lintnet.github.io/\n- https://github.com/lintnet-modules/ghalint\n\nlintnet is a general purpose linter powered by Jsonnet.\nWe've ported ghalint to [the lintnet module](https://github.com/lintnet-modules/ghalint), so you can migrate ghalint to lintnet!\n\n## Policies\n\n### 1. Workflow Policies\n\n1. [job_permissions](docs/policies/001.md): All jobs should have `permissions`\n1. [deny_read_all_permission](docs/policies/002.md): `read-all` permission should not be used\n1. [deny_write_all_permission](docs/policies/003.md): `write-all` permission should not be used\n1. [deny_inherit_secrets](docs/policies/004.md): `secrets: inherit` should not be used\n1. [workflow_secrets](docs/policies/005.md): Workflow should not set secrets to environment variables\n1. [job_secrets](docs/policies/006.md): Job should not set secrets to environment variables\n1. [deny_job_container_latest_image](docs/policies/007.md): Job's container image tag should not be `latest`\n1. [action_ref_should_be_full_length_commit_sha](docs/policies/008.md): action's ref should be full length commit SHA\n1. [github_app_should_limit_repositories](docs/policies/009.md): GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit repositories\n1. [github_app_should_limit_permissions](docs/policies/010.md): GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit permissions\n1. [job_timeout_minutes_is_required](docs/policies/012.md): All jobs should set [timeout-minutes](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes)\n1. [checkout_persist_credentials_should_be_false](docs/policies/013.md): [actions/checkout](https://github.com/actions/checkout)'s input `persist-credentials` should be `false`\n\n### 2. Action Policies\n\n1. [action_ref_should_be_full_length_commit_sha](docs/policies/008.md): action's ref should be full length commit SHA\n1. [github_app_should_limit_repositories](docs/policies/009.md): GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit repositories\n1. [github_app_should_limit_permissions](docs/policies/010.md): GitHub Actions issuing GitHub Access tokens from GitHub Apps should limit permissions\n1. [action_shell_is_required](docs/policies/011.md): `shell` is required if `run` is set\n1. [checkout_persist_credentials_should_be_false](docs/policies/013.md): [actions/checkout](https://github.com/actions/checkout)'s input `persist-credentials` should be `false`\n\n## How to use\n\n### 1. Validate workflows\n\nRun the command `ghalint run` on the repository root directory.\n\n```sh\nghalint run\n```\n\nThen ghalint validates workflow files `^\\.github/workflows/.*\\.ya?ml$`.\n\n### 2. Validate action.yaml\n\nRun the command `ghalint run-action`.\n\n```sh\nghalint run-action\n```\n\nThe alias `act` is available.\n\n```sh\nghalint act\n```\n\nThen ghalint validates action files `^([^/]+/){0,3}action\\.ya?ml$` on the current directory.\nYou can also specify file paths.\n\n```sh\nghalint act foo/action.yaml bar/action.yml\n```\n\n## Configuration file\n\nConfiguration file path: `^(\\.|\\.github/)?ghalint\\.ya?ml$`\n\nYou can specify the configuration file with the command line option `-config (-c)` or the environment variable `GHALINT_CONFIG`.\n\n```sh\nghalint -c foo.yaml run\n```\n\n### JSON Schema\n\n- [ghalint.json](json-schema/ghalint.json)\n- https://raw.githubusercontent.com/suzuki-shunsuke/ghalint/refs/heads/main/json-schema/ghalint.json\n\nIf you look for a CLI tool to validate configuration with JSON Schema, [ajv-cli](https://ajv.js.org/packages/ajv-cli.html) is useful.\n\n```sh\najv --spec=draft2020 -s json-schema/ghalint.json -d ghalint.yaml\n```\n\n#### Input Complementation by YAML Language Server\n\n[Please see the comment too.](https://github.com/szksh-lab/.github/issues/67#issuecomment-2564960491)\n\nVersion: `main`\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/ghalint/main/json-schema/ghalint.json\n```\n\nOr pinning version:\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/suzuki-shunsuke/ghalint/v1.2.1/json-schema/ghalint.json\n```\n\n### Disable policies\n\nYou can disable the following policies.\n\n- [deny_inherit_secrets](docs/policies/004.md)\n- [job_secrets](docs/policies/006.md)\n- [action_ref_should_be_full_length_commit_sha](docs/policies/008.md)\n- [github_app_should_limit_repositories](docs/policies/009.md)\n\ne.g.\n\n```yaml\nexcludes:\n  - policy_name: deny_inherit_secrets\n    workflow_file_path: .github/workflows/actionlint.yaml\n    job_name: actionlint\n  - policy_name: job_secrets\n    workflow_file_path: .github/workflows/actionlint.yaml\n    job_name: actionlint\n  - policy_name: action_ref_should_be_full_length_commit_sha\n    action_name: slsa-framework/slsa-github-generator\n  - policy_name: github_app_should_limit_repositories\n    workflow_file_path: .github/workflows/test.yaml\n    job_name: test\n    step_id: create_token\n```\n\n## Environment variables\n\n- `GHALINT_CONFIG`: Configuration file path\n- `GHALINT_LOG_LEVEL`: Log level One of `error`, `warn`, `info` (default), `debug`\n- `GHALINT_LOG_COLOR`: Configure log color. One of `auto` (default), `always`, and `never`.\n\n💡 If you want to enable log color in GitHub Actions, please try `GHALINT_LOG_COLOR=always` \n\n```yaml\nenv:\n  GHALINT_LOG_COLOR: always\n```\n\nAS IS\n\n<img width=\"986\" alt=\"image\" src=\"https://user-images.githubusercontent.com/13323303/216190768-cb09597f-5669-4907-b443-78d96b4491ab.png\">\n\nTO BE\n\n<img width=\"1023\" alt=\"image\" src=\"https://user-images.githubusercontent.com/13323303/216190842-0c015088-dda2-4e6f-8dbe-2db89cfbf438.png\">\n\n## How does it works?\n\nghalint reads GitHub Actions Workflows `^\\.github/workflows/.*\\.ya?ml$` and validates them.\nIf there are violatation ghalint outputs error logs and fails.\nIf there is no violation ghalint succeeds.\n\n## Experimental Features\n\n> [!WARNING]\n> These features are experimental, meaning they are unstable and may be changed or removed at minor or patch versions.\n\n### Validate inputs of actions and reusable workflows\n\n[#904](https://github.com/suzuki-shunsuke/ghalint/pull/904)\n\n```console\n$ ghalint exp validate-input\nERRO[0000] invalid input key                             action=suzuki-shunsuke/actionlint-action@c8d3c0dcc9152f1d1c7d4a38cbf4953c3a55953d input_key=actionlint_option job_key=actionlint program=ghalint required_inputs= valid_inputs=\"sparse-checkout, actionlint_options\" version=v1.0.0-local workflow_file_path=.github/workflows/actionlint.yaml\n```\n\n`ghalint exp validate-input` command validates inputs of actions and reusable workflows.\nIt fails if required inputs aren't given or unknown inputs are passed.\n\n> [!WARNING]\n> [Actions using `required: true` will not automatically return an error if the input is not specified.](https://docs.github.com/en/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#inputs)\n> This means if `ghalint exp validate-input` fails as required inputs aren't given, the action may work without any problem.\n> Now `ghalint exp validate-input` can't ignore those errors.\n> Ideally, actions should be fixed.\n\nBy default, the following files are validated.\n\n```\n.github/workflows/*.yaml\n.github/workflows/*.yml\naction.yaml\naction.yml\n*/action.yaml\n*/action.yml\n*/*/action.yaml\n*/*/action.yml\n*/*/*/action.yaml\n*/*/*/action.yml\n```\n\nThis command uses a GitHub access token with `contents:read` permission to download actions and reusable workflows.\nIt downloads them into `XDG_DATA_HOME/ghalint`.\nYou can pass a GitHub access token by environment variables `GITHUB_TOKEN` or `GHALINT_GITHUB_TOKEN`.\nYou can also manage it by secret stores such as GNOME Keyring, Windows Credential Manager, and macOS Keychain.\n\n```sh\nghalint exp token set [-stdin]\n```\n\n```sh\nghalint exp token rm # Remove a token from secret store\n```\n\n## LICENSE\n\n[MIT](LICENSE)\n"
  },
  {
    "path": "_typos.toml",
    "content": "[default.extend-words]\nERRO = \"ERRO\"\nintoto = \"intoto\"\n"
  },
  {
    "path": "aqua/aqua-checksums.json",
    "content": "{\n  \"checksums\": [\n    {\n      \"id\": \"github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_darwin_amd64.tar.gz\",\n      \"checksum\": \"C40ECE5407927327F94F35901727DBC604B46857E04F04EC94A310845FB71BDE\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_darwin_arm64.tar.gz\",\n      \"checksum\": \"24E4D34078AE81DA7C82539616F0CCAC3E226CF4F74A38CE6FB3463619E50A55\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_linux_amd64.tar.gz\",\n      \"checksum\": \"0E91737AEE2B5BAF1D255B959630194A302335D848FF97BB07921EB6205B5F5A\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_linux_arm64.tar.gz\",\n      \"checksum\": \"6F6CDCDC695721D91CE756E3B5BC3E3416599C464101F5E32E9C3F33054EE6D9\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/anchore/syft/v1.44.0/syft_1.44.0_windows_amd64.zip\",\n      \"checksum\": \"195E786EB84EC145854F20528992E86637C77D1968731DFE6CE850C90E28F47A\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-aarch64-apple-darwin.tar.gz\",\n      \"checksum\": \"4B15EE9548CD68CF22D6E67AF8A12CEB608EA4DBC34E0346792D09994222D694\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-aarch64-unknown-linux-musl.tar.gz\",\n      \"checksum\": \"311F2A15E8433C895CD9EE3198530BBFF552F59609EBA739F5BD9CEB2A2C0887\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-x86_64-apple-darwin.tar.gz\",\n      \"checksum\": \"3652F90D82D38F64E40C1791D2D82209979048EF3ABD715B0EB1488CF483CE1D\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-x86_64-pc-windows-msvc.zip\",\n      \"checksum\": \"DDC4AE26822E806CE84BC410643D02A3DAC53AAC9AB2A5F389624418C5654A17\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/crate-ci/typos/v1.46.2/typos-v1.46.2-x86_64-unknown-linux-musl.tar.gz\",\n      \"checksum\": \"D68C1A9C5ABD8DE11F7749EDFA414087C8BC828E89064714487D23C89F36B06E\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-darwin-amd64.tar.gz\",\n      \"checksum\": \"F6F06D94B6241521C53D15450C5209B028270BF966F842AFB11C030C79F5BC16\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-darwin-arm64.tar.gz\",\n      \"checksum\": \"A9C54498731B3128F79E090BE6110F3E5FFFCCC617B08142ED244D4126C73F29\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-linux-amd64.tar.gz\",\n      \"checksum\": \"8DF580D2670FED8FA984AAC0507099AF8DF275E665215F5C7A2AE3943893A553\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-linux-arm64.tar.gz\",\n      \"checksum\": \"44CD40A8C76C86755375ADFEEA52CFD3533CB43D7BD647771E0AE065E166DF3A\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-windows-amd64.zip\",\n      \"checksum\": \"BD42E3EBC8CB4ECECB86941983BAAF1DC221BBB04D838E94CE63B49CC91E02BB\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-windows-arm64.zip\",\n      \"checksum\": \"947B9A5BF762D465710B376C156F0184ABB2168378B0826AF1899E0EE7183742\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Darwin_all.tar.gz\",\n      \"checksum\": \"82D730F3366350C90D7E5DF3CF9E8E425FD1C84BF7D7E3E564F92D97C5EA9EA4\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Linux_arm64.tar.gz\",\n      \"checksum\": \"DE01CA1497571E9B348413CD2E7F74BE49B8D57696AE386F7EEDD06176544A88\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Linux_x86_64.tar.gz\",\n      \"checksum\": \"AAE00C71A4A6D55E08CCE9273A1516BDCE33C1E07CFFB7E502FA6FEC4377DEDE\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Windows_arm64.zip\",\n      \"checksum\": \"10227D9DE3EB846F0E58529C22E75DCBD713B67879A7F83912DE7ABE658C5FD7\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/goreleaser/goreleaser/v2.15.4/goreleaser_Windows_x86_64.zip\",\n      \"checksum\": \"146695F49717DFD79D64D5D6F4B1D25E2B56D73E723BBF68A8DC13CE5CF69693\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Darwin_arm64.tar.gz\",\n      \"checksum\": \"C28DEF83AF6C5AA8728D6D18160546AFD3E5A219117715A2C6C023BD16F14D10\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Darwin_x86_64.tar.gz\",\n      \"checksum\": \"9BAADB110C87F22C55688CF4A966ACCE3006C7A4A962732D6C8B45234C454C6E\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Linux_arm64.tar.gz\",\n      \"checksum\": \"B6AFF657B39E9267A258E8FA66D616F7221AEC5975D0251DAC76043BAD0FA177\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Linux_x86_64.tar.gz\",\n      \"checksum\": \"AD5CE7D5FFA52AAA7EC8710A8FA764181B6CECAAB843CC791E1CCE1680381569\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Windows_arm64.tar.gz\",\n      \"checksum\": \"72ABE9907454C5697777CFFF1D0D03DB8F5A9FD6950C609CA397A90D41AB65D7\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/reviewdog/reviewdog/v0.21.0/reviewdog_0.21.0_Windows_x86_64.tar.gz\",\n      \"checksum\": \"97C733E492DEC1FD83B9342C25A384D5AB6EBFA72B6978346E9A436CAD1853F6\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/sigstore/cosign/v3.0.6/cosign-darwin-amd64\",\n      \"checksum\": \"4C3E7AF8372D3CA3296E62FA56F23FCBB5721CC6AC1827900D398F110D7CD280\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/sigstore/cosign/v3.0.6/cosign-darwin-arm64\",\n      \"checksum\": \"5FADD012AE6381A6A29FF86A7D39AA873878852F1073FC90B15995961ECFB084\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/sigstore/cosign/v3.0.6/cosign-linux-amd64\",\n      \"checksum\": \"C956E5DFCAC53D52BCF058360D579472F0C1D2D9B69F55209E256FE7783F4C74\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/sigstore/cosign/v3.0.6/cosign-linux-arm64\",\n      \"checksum\": \"BEDAC92E8C3729864E13D4A17048007CFAFA79D5DECA993A43A90FFE018EF2B8\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/sigstore/cosign/v3.0.6/cosign-windows-amd64.exe\",\n      \"checksum\": \"9B85A88EBFF2D9DD30FF4984A6F61F2CEDC232DD87D81FA7F2FF3C0ED96C241C\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_darwin_amd64.tar.gz\",\n      \"checksum\": \"768B8517666A15D25A6870307231416016FC1165F8A1C1743B6AACDBAC7A5FAC\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_darwin_arm64.tar.gz\",\n      \"checksum\": \"FBD7DADDBB65ABD0DE5C6B898F2219588C7D1A71DF6808137D0A628858E7777B\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_linux_amd64.tar.gz\",\n      \"checksum\": \"40BC7B5F472211B22C4786D55F6859FA8093F1A373FF40A2DCCD29BD3D11CF96\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_linux_arm64.tar.gz\",\n      \"checksum\": \"691EB4CC3929A5E065F7C2F977CEE8306D817CB0F8DE9D5B4B4ED38C027CEC41\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_windows_amd64.zip\",\n      \"checksum\": \"4452010897556935E3F94A11AF2B2889563E05073A6DEA72FCF40B83B7F4AE5B\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/cmdx/v2.0.2/cmdx_windows_arm64.zip\",\n      \"checksum\": \"156D02F4E784E237B0661464D6FF76D6C4EFC4E01F858F8A9734364CD41BC98E\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_darwin_amd64.tar.gz\",\n      \"checksum\": \"D2A0E8605333068065DCF4C9B7B7A24891EDA1750AC01FB755DFBA426A390883\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_darwin_arm64.tar.gz\",\n      \"checksum\": \"1262CAC411E27B4653E6B66B7B06580EBCC2026FDD903E12E6CB0E4591639DE6\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_linux_amd64.tar.gz\",\n      \"checksum\": \"98EE0E3330DE7286F470D1E89C03FF7CE70D7A5998BA0F15969C400447BE579C\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_linux_arm64.tar.gz\",\n      \"checksum\": \"203A22C70B40BB161626973AD2A8DD06AEB736699FC8E03DD425DEE8FF3406E6\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_windows_amd64.zip\",\n      \"checksum\": \"109EA9B39C8E263CEF924BD3B4FE5505964204F934CA60D986F4090D01A99BA5\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"github_release/github.com/suzuki-shunsuke/ghalint/v1.5.6/ghalint_1.5.6_windows_arm64.zip\",\n      \"checksum\": \"C1219CAE104EA418A1CB4E7A02526A3FAD384C0788FA3540A86B316BB074D0D8\",\n      \"algorithm\": \"sha256\"\n    },\n    {\n      \"id\": \"registries/github_content/github.com/aquaproj/aqua-registry/v4.513.1/registry.yaml\",\n      \"checksum\": \"2F2D35FCFD79012DD744CD867EDFEDED4954A9382191C35BB25D776C35DECF3A\",\n      \"algorithm\": \"sha256\"\n    }\n  ]\n}\n"
  },
  {
    "path": "aqua/aqua.yaml",
    "content": "---\n# yaml-language-server: $schema=https://raw.githubusercontent.com/aquaproj/aqua/main/json-schema/aqua-yaml.json\n# aqua - Declarative CLI Version Manager\n# https://aquaproj.github.io/\nchecksum:\n  enabled: true\n  require_checksum: true\nregistries:\n  - type: standard\n    ref: v4.513.1  # renovate: depName=aquaproj/aqua-registry\nimport_dir: imports\n"
  },
  {
    "path": "aqua/imports/cmdx.yaml",
    "content": "packages:\n  - name: suzuki-shunsuke/cmdx@v2.0.2\n"
  },
  {
    "path": "aqua/imports/cosign.yaml",
    "content": "packages:\n  - name: sigstore/cosign@v3.0.6\n"
  },
  {
    "path": "aqua/imports/ghalint.yaml",
    "content": "packages:\n  - name: suzuki-shunsuke/ghalint@v1.5.6\n"
  },
  {
    "path": "aqua/imports/go-licenses.yaml",
    "content": "packages:\n  - name: google/go-licenses@v2.0.1\n"
  },
  {
    "path": "aqua/imports/golangci-lint.yaml",
    "content": "packages:\n  - name: golangci/golangci-lint@v2.12.2\n"
  },
  {
    "path": "aqua/imports/goreleaser.yaml",
    "content": "packages:\n  - name: goreleaser/goreleaser@v2.15.4\n"
  },
  {
    "path": "aqua/imports/reviewdog.yaml",
    "content": "packages:\n  - name: reviewdog/reviewdog@v0.21.0\n"
  },
  {
    "path": "aqua/imports/syft.yaml",
    "content": "packages:\n  - name: anchore/syft@v1.44.0\n"
  },
  {
    "path": "aqua/imports/typos.yaml",
    "content": "packages:\n  - name: crate-ci/typos@v1.46.2\n"
  },
  {
    "path": "cmd/gen-jsonschema/main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/suzuki-shunsuke/gen-go-jsonschema/jsonschema\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n)\n\nfunc main() {\n\tif err := core(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n\nfunc core() error {\n\tif err := jsonschema.Write(&config.Config{}, \"json-schema/ghalint.json\"); err != nil {\n\t\treturn fmt.Errorf(\"create or update a JSON Schema: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "cmd/ghalint/main.go",
    "content": "package main\n\nimport (\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/cli\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave\"\n)\n\nvar version = \"\"\n\nfunc main() {\n\turfave.Main(\"ghalint\", version, cli.Run)\n}\n"
  },
  {
    "path": "cmdx.yaml",
    "content": "---\n# cmdx - task runner\n# https://github.com/suzuki-shunsuke/cmdx\ntasks:\n  - name: test\n    short: t\n    description: test\n    usage: test\n    script: go test ./... -race -covermode=atomic\n  - name: coverage\n    short: c\n    description: coverage test\n    usage: coverage test\n    script: \"bash scripts/coverage.sh {{.target}}\"\n    args:\n      - name: target\n  - name: vet\n    short: v\n    description: go vet\n    usage: go vet\n    script: go vet ./...\n  - name: lint\n    short: l\n    description: lint the go code\n    usage: lint the go code\n    script: golangci-lint run\n  - name: install\n    short: i\n    description: go install\n    usage: go install\n    script: |\n      sha=\"\"\n      if git diff --quiet; then\n        sha=$(git rev-parse HEAD)\n      fi\n      go install \\\n        -ldflags \"-X main.version=v1.0.0-local -X main.commit=$sha -X main.date=$(date +\"%Y-%m-%dT%H:%M:%SZ%:z\" | tr -d '+')\" \\\n        ./cmd/ghalint\n  - name: usage\n    description: Update usage.md\n    usage: Update usage.md\n    script: bash scripts/generate-usage.sh\n  - name: js\n    description: Generate JSON Schema\n    usage: Generate JSON Schema\n    script: \"go run ./cmd/gen-jsonschema\"\n"
  },
  {
    "path": "docs/codes/001.md",
    "content": "# parse a workflow file as YAML: EOF\n\n```console\n$ ghalint run\nERRO[0000] read a workflow file                          error=\"parse a workflow file as YAML: EOF\" program=ghalint version=0.2.6 workflow_file_path=.github/workflows/test.yaml\n```\n\nThis error occurs if the workflow file has no YAML node.\nProbably this means the YAML file is empty or all codes are empty lines or commented out.\n\n## How to solve\n\n1. Fix the workflow file\n1. Move or rename the workflow file to exclude it from targets of ghalint\n\nIf this error occurs, probably the YAML file is invalid as a GitHub Actions Workflow.\nSo this isn't a bug of ghalint.\nPlease fix the workflow file.\n\nref. https://github.com/suzuki-shunsuke/ghalint/issues/197#issuecomment-1782032909\n\n<img width=\"1095\" alt=\"image\" src=\"https://github.com/suzuki-shunsuke/ghalint/assets/13323303/f471466c-6b87-415e-853c-115c3e76fded\">\n\n> [Error: .github#L1](https://github.com/suzuki-shunsuke/test-github-action/commit/52b75ce5cf55aeff15394fb0cabdbaaa28fab847#annotation_15218437727)\n> No event triggers defined in `on`\n"
  },
  {
    "path": "docs/codes/002.md",
    "content": "# read a configuration file: parse configuration file as YAML: EOF\n\n```console\n$ ghalint run\nFATA[0000] ghalint failed                                config_file=ghalint.yaml error=\"read a configuration file: parse configuration file as YAML: EOF\"\n```\n\nThis error occurs if the configuration file has no YAML node.\nProbably this means the YAML file is empty or all codes are empty lines or commented out.\n\n## How to solve\n\nPlease fix the configuration file.\n"
  },
  {
    "path": "docs/install.md",
    "content": "# Install\n\nghalint is written in Go. So you only have to install a binary in your `PATH`.\n\nThere are some ways to install ghalint.\n\n1. [Homebrew](#homebrew)\n1. [Scoop](#scoop)\n1. [aqua](#aqua)\n1. [mise](#mise)\n1. [GitHub Releases](#github-releases)\n1. [Build an executable binary from source code yourself using Go](#build-an-executable-binary-from-source-code-yourself-using-go)\n\n## Homebrew\n\nYou can install ghalint using [Homebrew](https://brew.sh/).\n\n```sh\nbrew install ghalint\n```\n\nOr\n\n```sh\nbrew install suzuki-shunsuke/ghalint/ghalint\n```\n\n## Scoop\n\nYou can install ghalint using [Scoop](https://scoop.sh/).\n\n```sh\nscoop bucket add suzuki-shunsuke https://github.com/suzuki-shunsuke/scoop-bucket\nscoop install ghalint\n```\n\n## aqua\n\nYou can install ghalint using [aqua](https://aquaproj.github.io/).\n\n```sh\naqua g -i suzuki-shunsuke/ghalint\n```\n\n## mise\n\nYou can install ghalint using [mise](https://github.com/jdx/mise).\n\n```sh\nmise use -g ghalint@latest\n```\n\n## Build an executable binary from source code yourself using Go\n\n```sh\ngo install github.com/suzuki-shunsuke/ghalint/cmd/ghalint@latest\n```\n\n## GitHub Releases\n\nYou can download an asset from [GitHub Releases](https://github.com/suzuki-shunsuke/ghalint/releases).\nPlease unarchive it and install a pre built binary into `$PATH`. \n\n### Verify downloaded assets from GitHub Releases\n\nYou can verify downloaded assets using some tools.\n\n1. [GitHub CLI](https://cli.github.com/)\n1. [slsa-verifier](https://github.com/slsa-framework/slsa-verifier)\n1. [Cosign](https://github.com/sigstore/cosign)\n\n### 1. GitHub CLI\n\nYou can install GitHub CLI by aqua.\n\n```sh\naqua g -i cli/cli\n```\n\n```sh\nversion=v1.2.0\nasset=ghalint_darwin_arm64.tar.gz\ngh release download -R suzuki-shunsuke/ghalint \"$version\" -p \"$asset\"\ngh attestation verify \"$asset\" \\\n  -R suzuki-shunsuke/ghalint \\\n  --signer-workflow suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml\n```\n\n### 2. slsa-verifier\n\nYou can install slsa-verifier by aqua.\n\n```sh\naqua g -i slsa-framework/slsa-verifier\n```\n\n```sh\nversion=v1.2.0\nasset=ghalint_darwin_arm64.tar.gz\ngh release download -R suzuki-shunsuke/ghalint \"$version\" -p \"$asset\" -p multiple.intoto.jsonl\nslsa-verifier verify-artifact \"$asset\" \\\n  --provenance-path multiple.intoto.jsonl \\\n  --source-uri github.com/suzuki-shunsuke/ghalint \\\n  --source-tag \"$version\"\n```\n\n### 3. Cosign\n\nYou can install Cosign by aqua.\n\n```sh\naqua g -i sigstore/cosign\n```\n\n```sh\nversion=v1.2.0\nchecksum_file=\"ghalint_${version#v}_checksums.txt\"\nasset=ghalint_darwin_arm64.tar.gz\ngh release download \"$version\" \\\n  -R suzuki-shunsuke/ghalint \\\n  -p \"$asset\" \\\n  -p \"$checksum_file\" \\\n  -p \"${checksum_file}.pem\" \\\n  -p \"${checksum_file}.sig\"\ncosign verify-blob \\\n  --signature \"${checksum_file}.sig\" \\\n  --certificate \"${checksum_file}.pem\" \\\n  --certificate-identity-regexp 'https://github\\.com/suzuki-shunsuke/go-release-workflow/\\.github/workflows/release\\.yaml@.*' \\\n  --certificate-oidc-issuer \"https://token.actions.githubusercontent.com\" \\\n  \"$checksum_file\"\ncat \"$checksum_file\" | sha256sum -c --ignore-missing\n```\n"
  },
  {
    "path": "docs/policies/001.md",
    "content": "# job_permissions\n\nAll jobs should have the field [permissions](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions).\n\n## Examples\n\n:x:\n\n```yaml\njobs:\n  foo: # The job doesn't have `permissions`\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo hello\n```\n\n:o:\n\n```yaml\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    permissions: {} # Set permissions\n    steps:\n      - run: echo hello\n```\n\n## Why?\n\nFor least privilege.\n\n## Exceptions\n\n1. workflow's `permissions` is empty `{}`\n\n```yaml\npermissions: {} # empty permissions\njobs:\n  foo: # The job is missing `permissions`, but it's okay because the workflow's `permissions` is empty\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo hello\n```\n\n2. workflow has only one job and the workflow has `permissions`\n\n```yaml\npermissions:\n  contents: read\njobs:\n  foo: # The job is missing `permissions`, but it's okay because the workflow has permissions and the workflow has only one job.\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo hello\n```\n"
  },
  {
    "path": "docs/policies/002.md",
    "content": "# deny_read_all_permission\n\n[`read-all` permission](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defining-access-for-the-github_token-scopes) should not be used.\n\n## Examples\n\n:x:\n\n```yaml\nname: test\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    permissions: read-all # Don't use read-all\n    steps:\n      - run: echo foo\n```\n\n:o:\n\n```yaml\nname: test\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n    steps:\n      - run: echo foo\n```\n\n## Why?\n\nFor least privilege.\nYou should grant only necessary permissions.\n"
  },
  {
    "path": "docs/policies/003.md",
    "content": "# deny_write_all_permission\n\n[`write-all` permission](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#defining-access-for-the-github_token-scopes) should not be used.\n\n## Examples\n\n:x:\n\n```yaml\nname: test\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    permissions: write-all # Don't use write-all\n    steps:\n      - run: echo foo\n```\n\n:o:\n\n```yaml\nname: test\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - run: echo foo\n```\n\n## Why?\n\nFor least privilege.\nYou should grant only necessary permissions.\n"
  },
  {
    "path": "docs/policies/004.md",
    "content": "# deny_inherit_secrets\n\n[`secrets: inherit`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idsecretsinherit) should not be used\n\n## Examples\n\n:x:\n\n```yaml\njobs:\n  release:\n    uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@v0.4.4\n    secrets: inherit # `inherit` should not be used\n```\n\n:o:\n\n```yaml\njobs:\n  release:\n    uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@v0.4.4\n    secrets: # Only required secrets should be passed\n      gh_app_id: ${{ secrets.APP_ID }}\n      gh_app_private_key: ${{ secrets.APP_PRIVATE_KEY }}\n```\n\n## Why?\n\nSecrets should be exposed to only required jobs.\n\n## How to ignore the violation\n\nWe don't recommend, but if you want to ignore the violation of this policy, please configure it with [the configuration file](../../README.md#configuration-file).\n\ne.g.\n\nghalint.yaml\n\n```yaml\nexcludes:\n  - policy_name: deny_inherit_secrets\n    workflow_file_path: .github/workflows/actionlint.yaml\n    job_name: actionlint\n```\n\n`policy_name`, `workflow_file_path`, and `job_name` are required.\n"
  },
  {
    "path": "docs/policies/005.md",
    "content": "# workflow_secrets\n\nWorkflows should not set secrets to environment variables.\n\n## Examples\n\n:x:\n\n```yaml\nname: test\nenv:\n  GITHUB_TOKEN: ${{github.token}} # The secret should not be set to workflow's environment variables \n  DATADOG_API_KEY: ${{secrets.DATADOG_API_KEY}} # The secret should not be set to workflow's environment variables \njobs:\n  foo:\n    runs-on: ubuntu-latest\n    permissions: {}\n    steps:\n      - run: echo foo\n  bar:\n    runs-on: ubuntu-latest\n    permissions: {}\n    steps:\n      - run: echo bar\n```\n\n:o:\n\n```yaml\nname: test\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    permissions: {}\n    env:\n      GITHUB_TOKEN: ${{github.token}}\n    steps:\n      - run: echo foo\n  bar:\n    runs-on: ubuntu-latest\n    permissions: {}\n    env:\n      DATADOG_API_KEY: ${{secrets.DATADOG_API_KEY}}\n    steps:\n      - run: echo bar\n```\n\n## How to fix\n\nSet secrets to jobs or steps.\n\n## Why?\n\nSecrets should be exposed to only necessary jobs or steps.\n\n## Exceptions\n\nWorkflow has only one job.\n"
  },
  {
    "path": "docs/policies/006.md",
    "content": "# job_secrets\n\nJob should not set secrets to environment variables.\n\n## Examples\n\n:x:\n\n```yaml\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    env:\n      GITHUB_TOKEN: ${{github.token}} # secret is set in job\n    steps:\n      - run: echo foo\n      - run: gh label create bug\n```\n\n:o:\n\n```yaml\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - run: echo foo\n      - run: gh label create bug\n        env:\n          GITHUB_TOKEN: ${{github.token}} # secret is set in step\n```\n\n## How to fix\n\nSet secrets to steps.\n\n## Why?\n\nSecrets should be exposed to only necessary steps.\n\n## Exceptions\n\nJob has only one step.\n\n## How to ignore the violation\n\nWe don't recommend, but if you want to ignore the violation of this policy, please configure it with [the configuration file](../../README.md#configuration-file).\n\ne.g.\n\nghalint.yaml\n\n```yaml\nexcludes:\n  - policy_name: job_secrets\n    workflow_file_path: .github/workflows/actionlint.yaml\n    job_name: actionlint\n```\n\n`policy_name`, `workflow_file_path`, and `job_name` are required.\n"
  },
  {
    "path": "docs/policies/007.md",
    "content": "# deny_job_container_latest_image\n\nJob's container image tag should not be `latest`.\n\n## Examples\n\n:x:\n\n```yaml\njobs:\n  container-test-job:\n    runs-on: ubuntu-latest\n    container:\n      image: node:latest # latest tags should not be used\n```\n\n⭕ \n\n```yaml\njobs:\n  container-test-job:\n    runs-on: ubuntu-latest\n    container:\n      image: node:10 # Ideally, hash is best\n```\n\n## Why?\n\nImage tags should be pinned with tag or hash.\n"
  },
  {
    "path": "docs/policies/008.md",
    "content": "# action_ref_should_be_full_length_commit_sha\n\naction's ref should be full length commit SHA\n\n## Examples\n\n:x:\n\n```\nactions/checkout@v3\n```\n\n⭕\n\n```\nactions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3\n```\n\n## Why?\n\nhttps://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-third-party-actions\n\n> Pinning an action to a full length commit SHA is currently the only way to use an action as an immutable release.\n> Pinning to a particular SHA helps mitigate the risk of a bad actor adding a backdoor to the action's repository, as they would need to generate a SHA-1 collision for a valid Git object payload\n\n## Exclude\n\nSome actions and reusable workflows don't support pinning version.\nYou can exclude those actions and reusable workflows.\n\nghalint.yaml\n\n```yaml\nexcludes:\n  # slsa-framework/slsa-github-generator doesn't support pinning version\n  # > Invalid ref: 68bad40844440577b33778c9f29077a3388838e9. Expected ref of the form refs/tags/vX.Y.Z\n  # https://github.com/slsa-framework/slsa-github-generator/issues/722\n  - policy_name: action_ref_should_be_full_length_commit_sha\n    action_name: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml\n```\n\n[#650](https://github.com/suzuki-shunsuke/ghalint/pull/650) As of v1.1.0, `action_name` supports a glob pattern.\n\nhttps://pkg.go.dev/path#Match\n\n```yaml\nexcludes:\n  - policy_name: action_ref_should_be_full_length_commit_sha\n    action_name: suzuki-shunsuke/tfaction/* # glob pattern\n```\n\n`policy_name` and `action_name` are mandatory.\n\n## pinact\n\nhttps://github.com/suzuki-shunsuke/pinact\n\n[pinact](https://github.com/suzuki-shunsuke/pinact) is useful to convert tags to full length commit SHA.\n"
  },
  {
    "path": "docs/policies/009.md",
    "content": "# github_app_should_limit_repositories\n\nGitHub Actions issuing GitHub Access tokens from GitHub Apps should limit repositories.\n\nThis policy supports the following actions.\n\n1. https://github.com/tibdex/github-app-token\n1. https://github.com/actions/create-github-app-token\n\n## Examples\n\n### tibdex/github-app-token\n\nhttps://github.com/tibdex/github-app-token\n\n:x:\n\n```yaml\n- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0\n  with:\n    app_id: ${{secrets.APP_ID}}\n    private_key: ${{secrets.PRIVATE_KEY}}\n```\n\n⭕\n\n```yaml\n- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0\n  with:\n    app_id: ${{secrets.APP_ID}}\n    private_key: ${{secrets.PRIVATE_KEY}}\n    repositories: >-\n      [\"${{github.event.repository.name}}\"]\n```\n\n### actions/create-github-app-token\n\nhttps://github.com/actions/create-github-app-token\n\n:x:\n\n```yaml\n- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0\n  with:\n    app-id: ${{vars.APP_ID}}\n    private-key: ${{secrets.PRIVATE_KEY}}\n    owner: ${{github.repository_owner}}\n    permission-issues: write\n```\n\n⭕\n\n```yaml\n- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0\n  with:\n    app-id: ${{vars.APP_ID}}\n    private-key: ${{secrets.PRIVATE_KEY}}\n    owner: ${{github.repository_owner}}\n    repositories: \"repo1,repo2\"\n    permission-issues: write\n```\n\nOr\n\n> If owner and repositories are empty, access will be scoped to only the current repository.\n\n```yaml\n- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0\n  with:\n    app-id: ${{vars.APP_ID}}\n    private-key: ${{secrets.PRIVATE_KEY}}\n    permission-issues: write\n```\n\n## Why?\n\nThe scope of access tokens should be limited.\n\n## How to ignore the violation\n\nWe don't recommend, but if you want to ignore the violation of this policy, please configure it with [the configuration file](../../README.md#configuration-file).\n\ne.g.\n\nghalint.yaml\n\n```yaml\nexcludes:\n  - policy_name: github_app_should_limit_repositories\n    workflow_file_path: .github/workflows/actionlint.yaml\n    job_name: actionlint\n    step_id: create_token\n```\n\n- workflow: `policy_name`, `workflow_file_path`, `job_name`, `step_id` are required.\n- action: `policy_name`, `action_file_path`, `step_id` are required.\n"
  },
  {
    "path": "docs/policies/010.md",
    "content": "# github_app_should_limit_permissions\n\nGitHub Actions issuing GitHub Access tokens from GitHub Apps should limit permissions.\n\nThis policy supports the following actions.\n\n1. https://github.com/tibdex/github-app-token\n1. https://github.com/actions/create-github-app-token\n\n> [!NOTE]\n> This policy has supported [actions/create-github-app-token](https://github.com/actions/create-github-app-token) since ghalint v1.3.0.\n> [actions/create-github-app-token](https://github.com/actions/create-github-app-token) has supported custom permissions since [v1.12.0](https://github.com/actions/create-github-app-token/releases/tag/v1.12.0).\n> If you use old create-github-app-token, please update it to v1.12.0 or later.\n\n## Examples\n\n### tibdex/github-app-token\n\nhttps://github.com/tibdex/github-app-token\n\n:x:\n\n```yaml\n- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0\n  with:\n    app_id: ${{secrets.APP_ID}}\n    private_key: ${{secrets.PRIVATE_KEY}}\n    repositories: >-\n      [\"${{github.event.repository.name}}\"]\n```\n\n⭕\n\n```yaml\n- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0\n  with:\n    app_id: ${{secrets.APP_ID}}\n    private_key: ${{secrets.PRIVATE_KEY}}\n    repositories: >-\n      [\"${{github.event.repository.name}}\"]\n    permissions: >-\n      {\n        \"contents\": \"read\"\n      }\n```\n\n### actions/create-github-app-token\n\n:x:\n\n```yaml\n- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0\n  with:\n    app-id: ${{vars.APP_ID}}\n    private-key: ${{secrets.PRIVATE_KEY}}\n```\n\n⭕\n\n```yaml\n- uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0\n  with:\n    app-id: ${{vars.APP_ID}}\n    private-key: ${{secrets.PRIVATE_KEY}}\n    permission-issues: write\n```\n\n## Why?\n\nThe scope of access tokens should be limited.\n"
  },
  {
    "path": "docs/policies/011.md",
    "content": "# action_shell_is_required\n\n`shell` is required if `run` is set\n\n## Examples\n\n:x:\n\n```yaml\n- run: echo hello\n```\n\n⭕\n\n```yaml\n- run: echo hello\n  shell: bash\n```\n\n## Why?\n\n> Required if run is set.\n\nhttps://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runsstepsshell\n"
  },
  {
    "path": "docs/policies/012.md",
    "content": "# job_timeout_minutes_is_required\n\nAll jobs should set [timeout-minutes](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes).\n\n## Examples\n\n:x:\n\n```yaml\njobs:\n  foo: # The job doesn't have `timeout-minutes`\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo hello\n```\n\n:o:\n\n```yaml\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    steps:\n      - run: echo hello\n```\n\n## :bulb: Set `timeout-minutes` by `ghatm`\n\nhttps://github.com/suzuki-shunsuke/ghatm\n\nIt's so bothersome to fix a lot of workflow files by hand.\n[ghatm](https://github.com/suzuki-shunsuke/ghatm) is a command line tool to fix them automatically.\n\n## Why?\n\nhttps://exercism.org/docs/building/github/gha-best-practices#h-set-timeouts-for-workflows\n\n> By default, GitHub Actions kills workflows after 6 hours if they have not finished by then. Many workflows don't need nearly as much time to finish, but sometimes unexpected errors occur or a job hangs until the workflow run is killed 6 hours after starting it. Therefore it's recommended to specify a shorter timeout.\n> \n> The ideal timeout depends on the individual workflow but 30 minutes is typically more than enough for the workflows used in Exercism repos.\n> \n> This has the following advantages:\n> \n> PRs won't be pending CI for half the day, issues can be caught early or workflow runs can be restarted.\n> The number of overall parallel builds is limited, hanging jobs will not cause issues for other PRs if they are cancelled early.\n\n## Exceptions\n\n1. All steps set `timeout-minutes`\n\n```yaml\njobs:\n  foo: # The job is missing `timeout-minutes`, but it's okay because all steps set timeout-minutes\n    runs-on: ubuntu-latest\n    steps:\n      - run: echo hello\n        timeout-minutes: 5\n      - run: echo bar\n        timeout-minutes: 5\n```\n\n2. A job uses a reusable workflow\n\nWhen a reusable workflow is called with `uses`, `timeout-minutes` is not available.\n\n```yaml\njobs:\n  foo:\n    uses: suzuki-shunsuke/renovate-config-validator-workflow/.github/workflows/validate.yaml@v0.2.3\n```\n"
  },
  {
    "path": "docs/policies/013.md",
    "content": "# checkout_persist_credentials_should_be_false\n\n[actions/checkout](https://github.com/actions/checkout)'s input `persist-credentials` should be `false`.\n\n## Examples\n\n:x:\n\n```yaml\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    steps:\n      # persist-credentials is not set\n      - uses: actions/checkout@v4\n\n  bar:\n    runs-on: ubuntu-latest\n    steps:\n      # persist-credentials is true\n      - uses: actions/checkout@v4\n        with:\n          persist-credentials: \"true\"\n```\n\n:o:\n\n```yaml\njobs:\n  foo:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          persist-credentials: \"false\"\n```\n\n## Why?\n\nhttps://github.com/actions/checkout/issues/485\n\nPersisting token allows every step after `actions/checkout` to access token.\nThis is a security risk.\n\n## :bulb: Fix using suzuki-shunsuke/disable-checkout-persist-credentials\n\nAdding `persist-credentials: false` by hand is bothersome.\nYou can do this automatically using suzuki-shunsuke/disable-checkout-persist-credentials.\n\nhttps://github.com/suzuki-shunsuke/disable-checkout-persist-credentials\n\n## How to ignore the violation\n\nIf you need to persist token in a specific job, please configure it with [the configuration file](../../README.md#configuration-file).\n\ne.g.\n\nghalint.yaml\n\n```yaml\nexcludes:\n  - policy_name: checkout_persist_credentials_should_be_false\n    workflow_file_path: .github/workflows/actionlint.yaml\n    job_name: actionlint\n```\n\n- workflow: `policy_name`, `workflow_file_path`, `job_name` are required\n- action: `policy_name` and `action_file_path` are required\n"
  },
  {
    "path": "docs/usage.md",
    "content": "# Usage\n\n<!-- This is generated by scripts/generate-usage.sh. Don't edit this file directly. -->\n\n```console\n$ ghalint --help\nNAME:\n   ghalint - GitHub Actions linter\n\nUSAGE:\n   ghalint [global options] [command [command options]]\n\nVERSION:\n   1.5.6\n\nCOMMANDS:\n   run              lint GitHub Actions Workflows\n   run-action, act  lint actions\n   experiment, exp  experimental commands\n   version          Show version\n   help, h          Shows a list of commands or help for one command\n   completion       Output shell completion script for bash, zsh, fish, or Powershell\n\nGLOBAL OPTIONS:\n   --log-color string          log color [$GHALINT_LOG_COLOR]\n   --log-level string          log level [$GHALINT_LOG_LEVEL]\n   --config string, -c string  configuration file path [$GHALINT_CONFIG]\n   --help, -h                  show help\n   --version, -v               print the version\n```\n\n## ghalint run\n\n```console\n$ ghalint run --help\nNAME:\n   ghalint run - lint GitHub Actions Workflows\n\nUSAGE:\n   ghalint run\n\nOPTIONS:\n   --help, -h  show help\n```\n\n## ghalint run-action\n\n```console\n$ ghalint run-action --help\nNAME:\n   ghalint run-action - lint actions\n\nUSAGE:\n   ghalint run-action [arguments...]\n\nOPTIONS:\n   --help, -h  show help\n```\n\n## ghalint experiment\n\n```console\n$ ghalint experiment --help\nNAME:\n   ghalint experiment - experimental commands\n\nUSAGE:\n   ghalint experiment [command [command options]]\n\nDESCRIPTION:\n   experimental commands. These commands are not stable and may change in the future without major updates.\n\nCOMMANDS:\n   validate-input  validate action inputs\n\nOPTIONS:\n   --help, -h  show help\n```\n\n### experiment validate-input\n\n```console\n$ experiment validate-input --help\nNAME:\n   ghalint experiment validate-input - validate action inputs\n\nUSAGE:\n   ghalint experiment validate-input\n\nDESCRIPTION:\n   validate action inputs\n\nOPTIONS:\n   --help, -h  show help\n```\n\n## ghalint version\n\n```console\n$ ghalint version --help\nNAME:\n   ghalint version - Show version\n\nUSAGE:\n   ghalint version\n\nOPTIONS:\n   --json, -j  Output version in JSON format\n   --help, -h  show help\n```\n\n## ghalint completion\n\n```console\n$ ghalint completion --help\nNAME:\n   ghalint completion - Output shell completion script for bash, zsh, fish, or Powershell\n\nUSAGE:\n   ghalint completion\n\nDESCRIPTION:\n   Output shell completion script for bash, zsh, fish, or Powershell.\n   Source the output to enable completion.\n\n   # .bashrc\n   source <(ghalint completion bash)\n\n   # .zshrc\n   source <(ghalint completion zsh)\n\n   # fish\n   ghalint completion fish > ~/.config/fish/completions/ghalint.fish\n\n   # Powershell\n   Output the script to path/to/autocomplete/ghalint.ps1 an run it.\n\n\nOPTIONS:\n   --help, -h  show help\n```"
  },
  {
    "path": "go.mod",
    "content": "module github.com/suzuki-shunsuke/ghalint\n\ngo 1.26.3\n\nrequire (\n\tgithub.com/adrg/xdg v0.5.3\n\tgithub.com/google/go-github/v86 v86.0.0\n\tgithub.com/spf13/afero v1.15.0\n\tgithub.com/suzuki-shunsuke/gen-go-jsonschema v0.1.0\n\tgithub.com/suzuki-shunsuke/slog-error v0.2.2\n\tgithub.com/suzuki-shunsuke/slog-util v0.3.2\n\tgithub.com/suzuki-shunsuke/urfave-cli-v3-util v0.2.3\n\tgithub.com/urfave/cli/v3 v3.9.0\n\tgolang.org/x/oauth2 v0.36.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tal.essio.dev/pkg/shellescape v1.5.1 // indirect\n\tgithub.com/bahlo/generic-list-go v0.2.0 // indirect\n\tgithub.com/buger/jsonparser v1.1.1 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/danieljoos/wincred v1.2.2 // indirect\n\tgithub.com/godbus/dbus/v5 v5.1.0 // indirect\n\tgithub.com/google/go-querystring v1.2.0 // indirect\n\tgithub.com/invopop/jsonschema v0.13.0 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/lmittmann/tint v1.1.3 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/suzuki-shunsuke/go-error-with-exit-code v1.0.0 // indirect\n\tgithub.com/wk8/go-ordered-map/v2 v2.1.8 // indirect\n\tgithub.com/zalando/go-keyring v0.2.6 // indirect\n\tgolang.org/x/sys v0.36.0 // indirect\n\tgolang.org/x/text v0.28.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=\nal.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=\ngithub.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=\ngithub.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=\ngithub.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=\ngithub.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=\ngithub.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=\ngithub.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=\ngithub.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-github/v86 v86.0.0 h1:S/6aANJhwRm8EQmGKVML3j41yq0h2BsTP8FnDkO7kcA=\ngithub.com/google/go-github/v86 v86.0.0/go.mod h1:zKv1l4SwDXNFMGByi2FWkq71KwSXqj/eQRZuqtmcot8=\ngithub.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=\ngithub.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=\ngithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=\ngithub.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=\ngithub.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=\ngithub.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/suzuki-shunsuke/gen-go-jsonschema v0.1.0 h1:g7askc+nskCkKRWTVOdsAT8nMhwiaVT6Dmlnh6uvITM=\ngithub.com/suzuki-shunsuke/gen-go-jsonschema v0.1.0/go.mod h1:yFO7h5wwFejxi6jbtazqmk7b/JSBxHcit8DGwb1bhg0=\ngithub.com/suzuki-shunsuke/go-error-with-exit-code v1.0.0 h1:oVXrrYNGBq4POyITQNWKzwsYz7B2nUcqtDbeX4BfeEc=\ngithub.com/suzuki-shunsuke/go-error-with-exit-code v1.0.0/go.mod h1:kDFtLeftDiIUUHXGI3xq5eJ+uAOi50FPrxPENTHktJ0=\ngithub.com/suzuki-shunsuke/slog-error v0.2.2 h1:z8rymlIlZcMA+ERnnhVigQ0Q+X0pxKqBfDzSIyGh6vU=\ngithub.com/suzuki-shunsuke/slog-error v0.2.2/go.mod h1:w45QyO2G0uiEuo9hhrcLqqRl3hmYon9jGgq9CrCxxOY=\ngithub.com/suzuki-shunsuke/slog-util v0.3.2 h1:P4sc/swT8rwmmKDfMrh9GR+AzYJhJdW3BSxZXYBURuY=\ngithub.com/suzuki-shunsuke/slog-util v0.3.2/go.mod h1:fHyN2kPkinXSgo6GMR0QBj0gd/CpSer0j8bc5C4Pqks=\ngithub.com/suzuki-shunsuke/urfave-cli-v3-util v0.2.3 h1:28ZzFUyh118PFMBeHuKYPkIwaxHo+/mJYmljlr9DBRU=\ngithub.com/suzuki-shunsuke/urfave-cli-v3-util v0.2.3/go.mod h1:pfMAEENW39YADk1hW/bfHfO4rMu8GKgO4Psh6YY9nyM=\ngithub.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c=\ngithub.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=\ngithub.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=\ngithub.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=\ngolang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=\ngolang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "json-schema/ghalint.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"https://github.com/suzuki-shunsuke/ghalint/pkg/config/config\",\n  \"$ref\": \"#/$defs/Config\",\n  \"$defs\": {\n    \"Config\": {\n      \"properties\": {\n        \"excludes\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/Exclude\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"type\": \"object\"\n    },\n    \"Exclude\": {\n      \"properties\": {\n        \"policy_name\": {\n          \"type\": \"string\"\n        },\n        \"workflow_file_path\": {\n          \"type\": \"string\"\n        },\n        \"action_file_path\": {\n          \"type\": \"string\"\n        },\n        \"job_name\": {\n          \"type\": \"string\"\n        },\n        \"action_name\": {\n          \"type\": \"string\"\n        },\n        \"step_id\": {\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"type\": \"object\",\n      \"required\": [\n        \"policy_name\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/action/find.go",
    "content": "package action\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/afero\"\n)\n\nfunc Find(fs afero.Fs) ([]string, error) {\n\tpatterns := []string{\n\t\t\"action.yaml\",\n\t\t\"action.yml\",\n\t\t\"*/action.yaml\",\n\t\t\"*/action.yml\",\n\t\t\"*/*/action.yaml\",\n\t\t\"*/*/action.yml\",\n\t\t\"*/*/*/action.yaml\",\n\t\t\"*/*/*/action.yml\",\n\t}\n\n\tfiles := []string{}\n\tfor _, pattern := range patterns {\n\t\tmatches, err := afero.Glob(fs, pattern)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"check if the action file exists: %w\", err)\n\t\t}\n\t\tfiles = append(files, matches...)\n\t}\n\treturn files, nil\n}\n"
  },
  {
    "path": "pkg/cli/app.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/cli/experiment\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/cli/experiment/validateinput\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/cli/gflags\"\n\t\"github.com/suzuki-shunsuke/slog-util/slogutil\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave\"\n\t\"github.com/urfave/cli/v3\"\n)\n\ntype RunArgs struct {\n\t*gflags.GlobalFlags\n}\n\ntype RunActionArgs struct {\n\t*gflags.GlobalFlags\n\n\tFiles []string\n}\n\nfunc Run(ctx context.Context, logger *slogutil.Logger, env *urfave.Env) error { //nolint:funlen\n\tfs := afero.NewOsFs()\n\trunner := &Runner{\n\t\tfs: fs,\n\t}\n\tgf := &gflags.GlobalFlags{}\n\trunArgs := &RunArgs{\n\t\tGlobalFlags: gf,\n\t}\n\trunActionArgs := &RunActionArgs{\n\t\tGlobalFlags: gf,\n\t}\n\tvalidateInputArgs := &validateinput.Args{\n\t\tGlobalFlags: gf,\n\t}\n\treturn urfave.Command(env, &cli.Command{ //nolint:wrapcheck\n\t\tName:  \"ghalint\",\n\t\tUsage: \"GitHub Actions linter\",\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:  \"log-color\",\n\t\t\t\tUsage: \"log color\",\n\t\t\t\tSources: cli.EnvVars(\n\t\t\t\t\t\"GHALINT_LOG_COLOR\",\n\t\t\t\t),\n\t\t\t\tDestination: &gf.LogColor,\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:  \"log-level\",\n\t\t\t\tUsage: \"log level\",\n\t\t\t\tSources: cli.EnvVars(\n\t\t\t\t\t\"GHALINT_LOG_LEVEL\",\n\t\t\t\t),\n\t\t\t\tDestination: &gf.LogLevel,\n\t\t\t},\n\t\t\t&cli.StringFlag{\n\t\t\t\tName:    \"config\",\n\t\t\t\tAliases: []string{\"c\"},\n\t\t\t\tUsage:   \"configuration file path\",\n\t\t\t\tSources: cli.EnvVars(\n\t\t\t\t\t\"GHALINT_CONFIG\",\n\t\t\t\t),\n\t\t\t\tDestination: &gf.Config,\n\t\t\t},\n\t\t},\n\t\tCommands: []*cli.Command{\n\t\t\t{\n\t\t\t\tName:  \"run\",\n\t\t\t\tUsage: \"lint GitHub Actions Workflows\",\n\t\t\t\tAction: func(ctx context.Context, _ *cli.Command) error {\n\t\t\t\t\treturn runner.Run(ctx, logger, runArgs)\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"run-action\",\n\t\t\t\tAliases: []string{\n\t\t\t\t\t\"act\",\n\t\t\t\t},\n\t\t\t\tUsage: \"lint actions\",\n\t\t\t\tAction: func(ctx context.Context, _ *cli.Command) error {\n\t\t\t\t\treturn runner.RunAction(ctx, logger, runActionArgs)\n\t\t\t\t},\n\t\t\t\tArguments: []cli.Argument{\n\t\t\t\t\t&cli.StringArgs{\n\t\t\t\t\t\tName:        \"files\",\n\t\t\t\t\t\tDestination: &runActionArgs.Files,\n\t\t\t\t\t\tMax:         -1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texperiment.New(logger, fs, validateInputArgs),\n\t\t},\n\t}).Run(ctx, env.Args)\n}\n\ntype Runner struct {\n\tfs afero.Fs\n}\n"
  },
  {
    "path": "pkg/cli/experiment/command.go",
    "content": "package experiment\n\nimport (\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/cli/experiment/validateinput\"\n\t\"github.com/suzuki-shunsuke/slog-util/slogutil\"\n\t\"github.com/urfave/cli/v3\"\n)\n\nfunc New(logger *slogutil.Logger, fs afero.Fs, validateInputArgs *validateinput.Args) *cli.Command {\n\treturn &cli.Command{\n\t\tName:        \"experiment\",\n\t\tAliases:     []string{\"exp\"},\n\t\tUsage:       \"experimental commands\",\n\t\tDescription: \"experimental commands. These commands are not stable and may change in the future without major updates.\",\n\t\tCommands: []*cli.Command{\n\t\t\tvalidateinput.New(logger, fs, validateInputArgs),\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "pkg/cli/experiment/validateinput/command.go",
    "content": "package validateinput\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/cli/gflags\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/controller/schema\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/github\"\n\t\"github.com/suzuki-shunsuke/slog-util/slogutil\"\n\t\"github.com/urfave/cli/v3\"\n)\n\ntype Args struct {\n\t*gflags.GlobalFlags\n}\n\nfunc New(logger *slogutil.Logger, fs afero.Fs, args *Args) *cli.Command {\n\trunner := &Runner{\n\t\tfs: fs,\n\t}\n\treturn &cli.Command{\n\t\tName:        \"validate-input\",\n\t\tUsage:       \"validate action inputs\",\n\t\tDescription: \"validate action inputs\",\n\t\tAction: func(ctx context.Context, _ *cli.Command) error {\n\t\t\treturn runner.Action(ctx, logger, args)\n\t\t},\n\t}\n}\n\ntype Runner struct {\n\tfs afero.Fs\n}\n\nfunc (r *Runner) Action(ctx context.Context, logger *slogutil.Logger, args *Args) error {\n\tif err := logger.SetLevel(args.LogLevel); err != nil {\n\t\treturn fmt.Errorf(\"set log level: %w\", err)\n\t}\n\tif err := logger.SetColor(args.LogColor); err != nil {\n\t\treturn fmt.Errorf(\"set log color: %w\", err)\n\t}\n\n\trootDir, err := GetRootDir()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get the root directory: %w\", err)\n\t}\n\n\tgh := github.New(ctx, logger.Logger)\n\n\tctrl := schema.New(r.fs, logger.Logger, gh.Repositories, rootDir)\n\n\treturn ctrl.Run(ctx) //nolint:wrapcheck\n}\n\nfunc GetRootDir() (string, error) {\n\t// ${GHALINT_ROOT_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/ghalint}\n\trootDir := os.Getenv(\"GHALINT_ROOT_DIR\")\n\tif rootDir != \"\" {\n\t\treturn rootDir, nil\n\t}\n\txdgDataHome := xdg.DataHome\n\tif xdgDataHome == \"\" {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"get the current user home directory: %w\", err)\n\t\t}\n\t\txdgDataHome = filepath.Join(home, \".local\", \"share\")\n\t}\n\treturn filepath.Join(xdgDataHome, \"ghalint\"), nil\n}\n"
  },
  {
    "path": "pkg/cli/gflags/gflags.go",
    "content": "package gflags\n\ntype GlobalFlags struct {\n\tLogColor string\n\tLogLevel string\n\tConfig   string\n}\n"
  },
  {
    "path": "pkg/cli/run.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/controller\"\n\t\"github.com/suzuki-shunsuke/slog-util/slogutil\"\n)\n\nfunc (r *Runner) Run(ctx context.Context, logger *slogutil.Logger, args *RunArgs) error {\n\tif err := logger.SetLevel(args.LogLevel); err != nil {\n\t\treturn fmt.Errorf(\"set log level: %w\", err)\n\t}\n\tif err := logger.SetColor(args.LogColor); err != nil {\n\t\treturn fmt.Errorf(\"set log color: %w\", err)\n\t}\n\n\tctrl := controller.New(r.fs)\n\n\treturn ctrl.Run(ctx, logger.Logger, args.Config) //nolint:wrapcheck\n}\n"
  },
  {
    "path": "pkg/cli/run_action.go",
    "content": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/controller/act\"\n\t\"github.com/suzuki-shunsuke/slog-util/slogutil\"\n)\n\nfunc (r *Runner) RunAction(ctx context.Context, logger *slogutil.Logger, args *RunActionArgs) error {\n\tif err := logger.SetColor(args.LogColor); err != nil {\n\t\treturn fmt.Errorf(\"set log color: %w\", err)\n\t}\n\tif err := logger.SetLevel(args.LogLevel); err != nil {\n\t\treturn fmt.Errorf(\"set log level: %w\", err)\n\t}\n\n\tctrl := act.New(r.fs)\n\n\treturn ctrl.Run(ctx, logger.Logger, args.Config, args.Files...) //nolint:wrapcheck\n}\n"
  },
  {
    "path": "pkg/config/config.go",
    "content": "package config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"path\"\n\t\"path/filepath\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype Config struct {\n\tExcludes []*Exclude `json:\"excludes,omitempty\"`\n}\n\ntype Exclude struct {\n\tPolicyName       string `json:\"policy_name\" yaml:\"policy_name\"`\n\tWorkflowFilePath string `json:\"workflow_file_path,omitempty\" yaml:\"workflow_file_path\"`\n\tActionFilePath   string `json:\"action_file_path,omitempty\" yaml:\"action_file_path\"`\n\tJobName          string `json:\"job_name,omitempty\" yaml:\"job_name\"`\n\tActionName       string `json:\"action_name,omitempty\" yaml:\"action_name\"`\n\tStepID           string `json:\"step_id,omitempty\" yaml:\"step_id\"`\n}\n\nfunc (e *Exclude) FilePath() string {\n\tif e.WorkflowFilePath != \"\" {\n\t\treturn e.WorkflowFilePath\n\t}\n\treturn e.ActionFilePath\n}\n\nfunc Find(fs afero.Fs) string {\n\tfilePaths := []string{\n\t\t\"ghalint.yaml\",\n\t\t\".ghalint.yaml\",\n\t\t\".github/ghalint.yaml\",\n\t\t\"ghalint.yml\",\n\t\t\".ghalint.yml\",\n\t\t\".github/ghalint.yml\",\n\t}\n\n\tfor _, filePath := range filePaths {\n\t\tif _, err := fs.Stat(filePath); err == nil {\n\t\t\treturn filePath\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc Read(fs afero.Fs, cfg *Config, filePath string) error {\n\tf, err := fs.Open(filePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"open a configuration file: %w\", err)\n\t}\n\tdefer f.Close()\n\tif err := yaml.NewDecoder(f).Decode(cfg); err != nil {\n\t\terr := fmt.Errorf(\"parse configuration file as YAML: %w\", err)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\treturn slogerr.With(err, \"reference\", \"https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/codes/002.md\") //nolint:wrapcheck\n\t\t}\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc Validate(cfg *Config) error {\n\tfor _, exclude := range cfg.Excludes {\n\t\tif err := validate(exclude); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ConvertPath(cfg *Config) {\n\tfor _, exclude := range cfg.Excludes {\n\t\tconvertPath(exclude)\n\t}\n}\n\nfunc convertPath(exclude *Exclude) {\n\texclude.WorkflowFilePath = filepath.FromSlash(exclude.WorkflowFilePath)\n\texclude.ActionFilePath = filepath.FromSlash(exclude.ActionFilePath)\n}\n\nfunc validate(exclude *Exclude) error { //nolint:cyclop\n\tif exclude.PolicyName == \"\" {\n\t\treturn errors.New(`policy_name is required`)\n\t}\n\tswitch exclude.PolicyName {\n\tcase \"action_ref_should_be_full_length_commit_sha\":\n\t\tif exclude.ActionName == \"\" {\n\t\t\treturn errors.New(`action_name is required to exclude action_ref_should_be_full_length_commit_sha`)\n\t\t}\n\t\tif _, err := path.Match(exclude.ActionName, \"\"); err != nil {\n\t\t\treturn fmt.Errorf(\"action_name must be a glob pattern: %w\", slogerr.With(err, \"pattern_reference\", \"https://pkg.go.dev/path#Match\"))\n\t\t}\n\tcase \"job_secrets\":\n\t\tif exclude.WorkflowFilePath == \"\" {\n\t\t\treturn errors.New(`workflow_file_path is required to exclude job_secrets`)\n\t\t}\n\t\tif exclude.JobName == \"\" {\n\t\t\treturn errors.New(`job_name is required to exclude job_secrets`)\n\t\t}\n\tcase \"deny_inherit_secrets\":\n\t\tif exclude.WorkflowFilePath == \"\" {\n\t\t\treturn errors.New(`workflow_file_path is required to exclude deny_inherit_secrets`)\n\t\t}\n\t\tif exclude.JobName == \"\" {\n\t\t\treturn errors.New(`job_name is required to exclude deny_inherit_secrets`)\n\t\t}\n\tcase \"github_app_should_limit_repositories\":\n\t\tif exclude.WorkflowFilePath == \"\" && exclude.ActionFilePath == \"\" {\n\t\t\treturn errors.New(`workflow_file_path or action_file_path is required to exclude github_app_should_limit_repositories`)\n\t\t}\n\t\tif exclude.WorkflowFilePath != \"\" && exclude.JobName == \"\" {\n\t\t\treturn errors.New(`job_name is required to exclude github_app_should_limit_repositories`)\n\t\t}\n\t\tif exclude.StepID == \"\" {\n\t\t\treturn errors.New(`step_id is required to exclude github_app_should_limit_repositories`)\n\t\t}\n\tcase \"checkout_persist_credentials_should_be_false\":\n\t\tif exclude.WorkflowFilePath == \"\" && exclude.ActionFilePath == \"\" {\n\t\t\treturn errors.New(`workflow_file_path or action_file_path is required to exclude checkout_persist_credentials_should_be_false`)\n\t\t}\n\t\tif exclude.WorkflowFilePath != \"\" && exclude.JobName == \"\" {\n\t\t\treturn errors.New(`job_name is required to exclude checkout_persist_credentials_should_be_false`)\n\t\t}\n\tdefault:\n\t\treturn slogerr.With(errors.New(`the policy can't be excluded`), \"policy_name\", exclude.PolicyName) //nolint:wrapcheck\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/config/config_test.go",
    "content": "package config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n)\n\nfunc TestValidate(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname  string\n\t\tcfg   *config.Config\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"policy_name is required\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"action_name is required\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"workflow_file_path is required\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"job_secrets\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"job_name is required\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName:       \"job_secrets\",\n\t\t\t\t\t\tWorkflowFilePath: \".github/workflows/foo.yaml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"disallowed policy\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName:       \"deny_read_all_permission\",\n\t\t\t\t\t\tWorkflowFilePath: \".github/workflows/foo.yaml\",\n\t\t\t\t\t\tJobName:          \"foo\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t}\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := config.Validate(d.cfg); err != nil {\n\t\t\t\tif d.isErr {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/controller/act/controller.go",
    "content": "package act\n\nimport (\n\t\"github.com/spf13/afero\"\n)\n\ntype Controller struct {\n\tfs afero.Fs\n}\n\nfunc New(fs afero.Fs) *Controller {\n\treturn &Controller{\n\t\tfs: fs,\n\t}\n}\n"
  },
  {
    "path": "pkg/controller/act/run.go",
    "content": "package act\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/action\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/controller\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave\"\n)\n\nfunc (c *Controller) Run(_ context.Context, logger *slog.Logger, cfgFilePath string, args ...string) error {\n\tcfg := &config.Config{}\n\tif err := c.readConfig(cfg, cfgFilePath); err != nil {\n\t\treturn err\n\t}\n\n\tfilePaths, err := c.listFiles(args...)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"find action files: %w\", err)\n\t}\n\tstepPolicies := []controller.StepPolicy{\n\t\t&policy.GitHubAppShouldLimitRepositoriesPolicy{},\n\t\t&policy.GitHubAppShouldLimitPermissionsPolicy{},\n\t\t&policy.ActionShellIsRequiredPolicy{},\n\t\tpolicy.NewActionRefShouldBeSHAPolicy(),\n\t\t&policy.CheckoutPersistCredentialShouldBeFalsePolicy{},\n\t}\n\tfailed := false\n\tfor _, filePath := range filePaths {\n\t\tlogger := logger.With(\"action_file_path\", filePath)\n\t\tif c.validateAction(logger, cfg, stepPolicies, filePath) {\n\t\t\tfailed = true\n\t\t}\n\t}\n\tif failed {\n\t\treturn urfave.ErrSilent\n\t}\n\treturn nil\n}\n\nfunc (c *Controller) listFiles(args ...string) ([]string, error) {\n\tif len(args) != 0 {\n\t\treturn args, nil\n\t}\n\n\treturn action.Find(c.fs) //nolint:wrapcheck\n}\n\nfunc (c *Controller) validateAction(logger *slog.Logger, cfg *config.Config, stepPolicies []controller.StepPolicy, filePath string) bool {\n\taction := &workflow.Action{}\n\tif err := workflow.ReadAction(c.fs, filePath, action); err != nil {\n\t\tslogerr.WithError(logger, err).Error(\"read an action file\")\n\t\treturn true\n\t}\n\n\tstepCtx := &policy.StepContext{\n\t\tFilePath: filePath,\n\t\tAction:   action,\n\t}\n\n\treturn c.applyStepPolicies(logger, cfg, stepCtx, action, stepPolicies)\n}\n\ntype Policy interface {\n\tName() string\n\tID() string\n}\n\nfunc withPolicyReference(logger *slog.Logger, p Policy) *slog.Logger {\n\treturn logger.With(\n\t\t\"policy_name\", p.Name(),\n\t\t\"reference\", fmt.Sprintf(\"https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/policies/%s.md\", p.ID()),\n\t)\n}\n\nfunc (c *Controller) applyStepPolicies(logger *slog.Logger, cfg *config.Config, stepCtx *policy.StepContext, action *workflow.Action, stepPolicies []controller.StepPolicy) bool {\n\tfailed := false\n\tfor _, stepPolicy := range stepPolicies {\n\t\tlogger := withPolicyReference(logger, stepPolicy)\n\t\tif c.applyStepPolicy(logger, cfg, stepCtx, action, stepPolicy) {\n\t\t\tfailed = true\n\t\t}\n\t}\n\treturn failed\n}\n\nfunc (c *Controller) applyStepPolicy(logger *slog.Logger, cfg *config.Config, stepCtx *policy.StepContext, action *workflow.Action, stepPolicy controller.StepPolicy) bool {\n\tfailed := false\n\tfor _, step := range action.Runs.Steps {\n\t\tlogger := logger\n\t\tif step.ID != \"\" {\n\t\t\tlogger = logger.With(\"step_id\", step.ID)\n\t\t}\n\t\tif step.Name != \"\" {\n\t\t\tlogger = logger.With(\"step_name\", step.Name)\n\t\t}\n\t\tif err := stepPolicy.ApplyStep(logger, cfg, stepCtx, step); err != nil {\n\t\t\tif err.Error() != \"\" {\n\t\t\t\tslogerr.WithError(logger, err).Error(\"the step violates policies\")\n\t\t\t}\n\t\t\tfailed = true\n\t\t}\n\t}\n\treturn failed\n}\n\nfunc (c *Controller) readConfig(cfg *config.Config, cfgFilePath string) error {\n\tif cfgFilePath == \"\" {\n\t\tif c := config.Find(c.fs); c != \"\" {\n\t\t\tcfgFilePath = c\n\t\t}\n\t}\n\tif cfgFilePath != \"\" {\n\t\tif err := config.Read(c.fs, cfg, cfgFilePath); err != nil {\n\t\t\treturn fmt.Errorf(\"read a configuration file: %w\", slogerr.With(err,\n\t\t\t\t\"config_file\", cfgFilePath,\n\t\t\t))\n\t\t}\n\t\tif err := config.Validate(cfg); err != nil {\n\t\t\treturn fmt.Errorf(\"validate a configuration file: %w\", slogerr.With(err,\n\t\t\t\t\"config_file\", cfgFilePath,\n\t\t\t))\n\t\t}\n\t\tconfig.ConvertPath(cfg)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/controller/controller.go",
    "content": "package controller\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype Controller struct {\n\tfs afero.Fs\n}\n\nfunc New(fs afero.Fs) *Controller {\n\treturn &Controller{\n\t\tfs: fs,\n\t}\n}\n\ntype WorkflowPolicy interface {\n\tName() string\n\tID() string\n\tApplyWorkflow(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, wf *workflow.Workflow) error\n}\n\ntype JobPolicy interface {\n\tName() string\n\tID() string\n\tApplyJob(logger *slog.Logger, cfg *config.Config, jobCtx *policy.JobContext, job *workflow.Job) error\n}\n\ntype StepPolicy interface {\n\tName() string\n\tID() string\n\tApplyStep(logger *slog.Logger, cfg *config.Config, stepCtx *policy.StepContext, step *workflow.Step) error\n}\n"
  },
  {
    "path": "pkg/controller/run.go",
    "content": "package controller\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave\"\n)\n\nfunc (c *Controller) Run(_ context.Context, logger *slog.Logger, cfgFilePath string) error {\n\tcfg := &config.Config{}\n\tif err := c.readConfig(cfg, cfgFilePath); err != nil {\n\t\treturn err\n\t}\n\n\tfilePaths, err := workflow.List(c.fs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"find workflow files: %w\", err)\n\t}\n\twfPolicies := []WorkflowPolicy{\n\t\tpolicy.NewWorkflowSecretsPolicy(),\n\t}\n\tjobPolicies := []JobPolicy{\n\t\t&policy.JobPermissionsPolicy{},\n\t\t&policy.JobTimeoutMinutesIsRequiredPolicy{},\n\t\tpolicy.NewJobSecretsPolicy(),\n\t\t&policy.DenyInheritSecretsPolicy{},\n\t\t&policy.DenyJobContainerLatestImagePolicy{},\n\t\tpolicy.NewActionRefShouldBeSHAPolicy(),\n\t\t&policy.DenyReadAllPermissionPolicy{},\n\t\t&policy.DenyWriteAllPermissionPolicy{},\n\t}\n\tstepPolicies := []StepPolicy{\n\t\t&policy.GitHubAppShouldLimitRepositoriesPolicy{},\n\t\t&policy.GitHubAppShouldLimitPermissionsPolicy{},\n\t\tpolicy.NewActionRefShouldBeSHAPolicy(),\n\t\t&policy.CheckoutPersistCredentialShouldBeFalsePolicy{},\n\t}\n\tfailed := false\n\tfor _, filePath := range filePaths {\n\t\tlogger := logger.With(\"workflow_file_path\", filePath)\n\t\tif c.validateWorkflow(logger, cfg, wfPolicies, jobPolicies, stepPolicies, filePath) {\n\t\t\tfailed = true\n\t\t}\n\t}\n\tif failed {\n\t\treturn urfave.ErrSilent\n\t}\n\treturn nil\n}\n\nfunc (c *Controller) validateWorkflow(logger *slog.Logger, cfg *config.Config, wfPolicies []WorkflowPolicy, jobPolicies []JobPolicy, stepPolicies []StepPolicy, filePath string) bool {\n\twf := &workflow.Workflow{\n\t\tFilePath: filePath,\n\t}\n\tif err := workflow.Read(c.fs, filePath, wf); err != nil {\n\t\tslogerr.WithError(logger, err).Error(\"read a workflow file\")\n\t\treturn true\n\t}\n\n\twfCtx := &policy.WorkflowContext{\n\t\tFilePath: filePath,\n\t\tWorkflow: wf,\n\t}\n\n\tfailed := false\n\tfor _, wfPolicy := range wfPolicies {\n\t\tlogger := withPolicyReference(logger, wfPolicy)\n\t\tif err := wfPolicy.ApplyWorkflow(logger, cfg, wfCtx, wf); err != nil {\n\t\t\tif err.Error() != \"\" {\n\t\t\t\tslogerr.WithError(logger, err).Error(\"the workflow violates policies\")\n\t\t\t}\n\t\t\tfailed = true\n\t\t\tcontinue\n\t\t}\n\t}\n\n\tif c.applyJobPolicies(logger, cfg, wfCtx, jobPolicies) {\n\t\tfailed = true\n\t}\n\n\tif c.applyStepPolicies(logger, cfg, wfCtx, wf.Jobs, stepPolicies) {\n\t\tfailed = true\n\t}\n\n\treturn failed\n}\n\ntype Policy interface {\n\tName() string\n\tID() string\n}\n\nfunc withPolicyReference(logger *slog.Logger, p Policy) *slog.Logger {\n\treturn logger.With(\n\t\t\"policy_name\", p.Name(),\n\t\t\"reference\", fmt.Sprintf(\"https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/policies/%s.md\", p.ID()),\n\t)\n}\n\nfunc (c *Controller) applyJobPolicies(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, jobPolicies []JobPolicy) bool {\n\tfailed := false\n\tfor _, jobPolicy := range jobPolicies {\n\t\tlogger := withPolicyReference(logger, jobPolicy)\n\t\tif c.applyJobPolicy(logger, cfg, wfCtx, jobPolicy) {\n\t\t\tfailed = true\n\t\t}\n\t}\n\treturn failed\n}\n\nfunc (c *Controller) applyJobPolicy(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, jobPolicy JobPolicy) bool {\n\tfailed := false\n\tfor jobName, job := range wfCtx.Workflow.Jobs {\n\t\tjobCtx := &policy.JobContext{\n\t\t\tWorkflow: wfCtx,\n\t\t\tName:     jobName,\n\t\t}\n\t\tlogger := logger.With(\"job_name\", jobName)\n\t\tif err := jobPolicy.ApplyJob(logger, cfg, jobCtx, job); err != nil {\n\t\t\tfailed = true\n\t\t\tif err.Error() != \"\" {\n\t\t\t\tslogerr.WithError(logger, err).Error(\"the job violates policies\")\n\t\t\t}\n\t\t}\n\t}\n\treturn failed\n}\n\nfunc (c *Controller) applyStepPolicies(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, jobs map[string]*workflow.Job, stepPolicies []StepPolicy) bool {\n\tfailed := false\n\tfor _, stepPolicy := range stepPolicies {\n\t\tlogger := withPolicyReference(logger, stepPolicy)\n\t\tif c.applyStepPolicy(logger, cfg, wfCtx, jobs, stepPolicy) {\n\t\t\tfailed = true\n\t\t}\n\t}\n\treturn failed\n}\n\nfunc (c *Controller) applyStepPolicy(logger *slog.Logger, cfg *config.Config, wfCtx *policy.WorkflowContext, jobs map[string]*workflow.Job, stepPolicy StepPolicy) bool {\n\tfailed := false\n\tfor jobName, job := range jobs {\n\t\tstepCtx := &policy.StepContext{\n\t\t\tFilePath: wfCtx.FilePath,\n\t\t\tJob: &policy.JobContext{\n\t\t\t\tName:     jobName,\n\t\t\t\tWorkflow: wfCtx,\n\t\t\t\tJob:      job,\n\t\t\t},\n\t\t}\n\t\tlogger := logger.With(\"job_name\", jobName)\n\t\tfor _, step := range job.Steps {\n\t\t\tlogger := logger\n\t\t\tif step.ID != \"\" {\n\t\t\t\tlogger = logger.With(\"step_id\", step.ID)\n\t\t\t}\n\t\t\tif step.Name != \"\" {\n\t\t\t\tlogger = logger.With(\"step_name\", step.Name)\n\t\t\t}\n\t\t\tif err := stepPolicy.ApplyStep(logger, cfg, stepCtx, step); err != nil {\n\t\t\t\tif err.Error() != \"\" {\n\t\t\t\t\tslogerr.WithError(logger, err).Error(\"the step violates policies\")\n\t\t\t\t}\n\t\t\t\tfailed = true\n\t\t\t}\n\t\t}\n\t}\n\treturn failed\n}\n\nfunc (c *Controller) readConfig(cfg *config.Config, cfgFilePath string) error {\n\tif cfgFilePath == \"\" {\n\t\tif c := config.Find(c.fs); c != \"\" {\n\t\t\tcfgFilePath = c\n\t\t}\n\t}\n\tif cfgFilePath != \"\" {\n\t\tif err := config.Read(c.fs, cfg, cfgFilePath); err != nil {\n\t\t\treturn fmt.Errorf(\"read a configuration file: %w\", slogerr.With(err,\n\t\t\t\t\"config_file\", cfgFilePath,\n\t\t\t))\n\t\t}\n\t\tif err := config.Validate(cfg); err != nil {\n\t\t\treturn fmt.Errorf(\"validate a configuration file: %w\", slogerr.With(err,\n\t\t\t\t\"config_file\", cfgFilePath,\n\t\t\t))\n\t\t}\n\t\tconfig.ConvertPath(cfg)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/controller/schema/action.go",
    "content": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/action\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n)\n\nfunc (c *Controller) runActions(ctx context.Context) error {\n\tfilePaths, err := action.Find(c.fs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"find action files: %w\", err)\n\t}\n\tfailed := false\n\tfor _, filePath := range filePaths {\n\t\tlogger := c.logger.With(\"action_file_path\", filePath)\n\t\tvw := &validateAction{\n\t\t\taction:  filePath,\n\t\t\tlogger:  logger,\n\t\t\tfs:      c.fs,\n\t\t\tgh:      c.gh,\n\t\t\trootDir: c.rootDir,\n\t\t}\n\t\tif err := vw.validate(ctx); err != nil {\n\t\t\tslogerr.WithError(logger, err).Error(\"validate action\")\n\t\t\tfailed = true\n\t\t}\n\t}\n\tif failed {\n\t\treturn errors.New(\"some action files are invalid\")\n\t}\n\treturn nil\n}\n\ntype validateAction struct {\n\taction  string\n\tlogger  *slog.Logger\n\tfs      afero.Fs\n\tgh      GitHub\n\trootDir string\n}\n\nfunc (v *validateAction) validate(ctx context.Context) error {\n\tact := &workflow.Action{}\n\tif err := workflow.ReadAction(v.fs, v.action, act); err != nil {\n\t\treturn fmt.Errorf(\"read an action file: %w\", err)\n\t}\n\tfailed := false\n\tfor _, step := range act.Runs.Steps {\n\t\tvs := &validateStep{\n\t\t\tstep:    step,\n\t\t\tlogger:  v.logger,\n\t\t\tfs:      v.fs,\n\t\t\tgh:      v.gh,\n\t\t\trootDir: v.rootDir,\n\t\t}\n\t\tif err := vs.validate(ctx); err != nil {\n\t\t\tslogerr.WithError(v.logger, err).Error(\"validate a step\")\n\t\t\tfailed = true\n\t\t}\n\t}\n\tif failed {\n\t\treturn errors.New(\"some steps are invalid\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/controller/schema/controller.go",
    "content": "package schema\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/github\"\n)\n\ntype Controller struct {\n\tfs      afero.Fs\n\tlogger  *slog.Logger\n\tgh      GitHub\n\trootDir string\n}\n\nfunc New(fs afero.Fs, logger *slog.Logger, gh GitHub, rootDir string) *Controller {\n\treturn &Controller{\n\t\tfs:      fs,\n\t\tlogger:  logger,\n\t\tgh:      gh,\n\t\trootDir: rootDir,\n\t}\n}\n\ntype GitHub interface {\n\tGetCommitSHA1(ctx context.Context, owner, repo, ref, lastSHA string) (string, *github.Response, error)\n\tGetContents(ctx context.Context, owner, repo, path string, opts *github.RepositoryContentGetOptions) (fileContent *github.RepositoryContent, directoryContent []*github.RepositoryContent, resp *github.Response, err error)\n}\n"
  },
  {
    "path": "pkg/controller/schema/job.go",
    "content": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave\"\n)\n\ntype validateJob struct {\n\tjob     *workflow.Job\n\tlogger  *slog.Logger\n\tfs      afero.Fs\n\tgh      GitHub\n\trootDir string\n}\n\nfunc (v *validateJob) validate(ctx context.Context) error {\n\t// Get actions\n\tif v.job.Uses != \"\" {\n\t\tv.logger = v.logger.With(\"reusable_workflow\", v.job.Uses)\n\t\tif err := v.validateReusableWorkflow(ctx); err != nil {\n\t\t\treturn fmt.Errorf(\"validate a reusable workflow: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\tfailed := false\n\tfor _, step := range v.job.Steps {\n\t\tvs := &validateStep{\n\t\t\tstep:    step,\n\t\t\tfs:      v.fs,\n\t\t\tlogger:  v.logger,\n\t\t\tgh:      v.gh,\n\t\t\trootDir: v.rootDir,\n\t\t}\n\t\tif err := vs.validate(ctx); err != nil {\n\t\t\tfailed = true\n\t\t\tif !errors.Is(err, urfave.ErrSilent) {\n\t\t\t\tslogerr.WithError(v.logger, err).Error(\"validate a step\")\n\t\t\t}\n\t\t}\n\t}\n\tif failed {\n\t\treturn urfave.ErrSilent\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/controller/schema/reusable_workflow.go",
    "content": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/github\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc (v *validateJob) validateReusableWorkflow(ctx context.Context) error {\n\t// read workflow\n\twf := &ReusableWorkflow{}\n\tif err := v.read(ctx, wf); err != nil {\n\t\treturn fmt.Errorf(\"read a reusable workflow: %w\", err)\n\t}\n\tif err := v.validateWorkflow(wf); err != nil {\n\t\treturn fmt.Errorf(\"validate a reusable workflow: %w\", err)\n\t}\n\treturn nil\n}\n\n/*\non:\n  workflow_call:\n    inputs:\n      aqua_policy_config:\n        required: false\n        type: string\n*/\n\ntype ReusableWorkflow struct {\n\tOn *On\n}\n\ntype On struct {\n\tWorkflowCall *WorkflowCall `yaml:\"workflow_call\"`\n}\n\nfunc (o *On) UnmarshalYAML(unmarshal func(any) error) error { //nolint:cyclop\n\tvar onAny any\n\tif err := unmarshal(&onAny); err != nil {\n\t\treturn fmt.Errorf(\"unmarshal a workflow to any: %w\", err)\n\t}\n\tif s, ok := onAny.(string); ok {\n\t\tif s != \"workflow_call\" {\n\t\t\treturn nil\n\t\t}\n\t\to.WorkflowCall = &WorkflowCall{}\n\t\treturn nil\n\t}\n\tonMap, ok := onAny.(map[string]any)\n\tif !ok {\n\t\treturn errors.New(\"failed to convert workflow on into map\")\n\t}\n\tworkflowCallAny, ok := onMap[\"workflow_call\"]\n\tif !ok {\n\t\treturn nil\n\t}\n\to.WorkflowCall = &WorkflowCall{}\n\tworkflowCallMap, ok := workflowCallAny.(map[string]any)\n\tif !ok {\n\t\treturn nil\n\t}\n\tinputsAny, ok := workflowCallMap[\"inputs\"]\n\tif !ok {\n\t\treturn nil\n\t}\n\tinputsMap, ok := inputsAny.(map[string]any)\n\tif !ok {\n\t\treturn nil\n\t}\n\to.WorkflowCall.Inputs = map[string]*workflow.Input{}\n\tfor inputKey, v := range inputsMap {\n\t\to.WorkflowCall.Inputs[inputKey] = &workflow.Input{}\n\t\tinputValueMap, ok := v.(map[string]any)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\trequiredAny, ok := inputValueMap[\"required\"]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\trequired, ok := requiredAny.(bool)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\to.WorkflowCall.Inputs[inputKey] = &workflow.Input{\n\t\t\tRequired: required,\n\t\t}\n\t}\n\treturn nil\n}\n\ntype WorkflowCall struct {\n\tInputs map[string]*workflow.Input\n}\n\nfunc (v *validateJob) validateWorkflow(wf *ReusableWorkflow) error {\n\tif wf.On == nil {\n\t\treturn errors.New(\"the reusable workflow is invalid. on is not set\")\n\t}\n\tif wf.On.WorkflowCall == nil {\n\t\treturn errors.New(\"the reusable workflow is invalid. workflow_call is not set\")\n\t}\n\tinputs := wf.On.WorkflowCall.Inputs\n\trequiredKeys := map[string]struct{}{}\n\tfor key, input := range inputs {\n\t\tif input.Required {\n\t\t\trequiredKeys[key] = struct{}{}\n\t\t}\n\t}\n\tv.logger = v.logger.With(\n\t\t\"valid_inputs\", strings.Join(slices.Collect(maps.Keys(inputs)), \", \"),\n\t\t\"required_inputs\", strings.Join(slices.Collect(maps.Keys(requiredKeys)), \", \"),\n\t)\n\tfailed := false\n\t// Check if the input is valid\n\tfor key := range v.job.With {\n\t\tif _, ok := inputs[key]; !ok {\n\t\t\tv.logger.Error(\"invalid input key\", \"input_key\", key)\n\t\t\tfailed = true\n\t\t}\n\t}\n\t// Check if required keys are set\n\tfor key := range requiredKeys {\n\t\tif _, ok := v.job.With[key]; !ok {\n\t\t\tv.logger.Error(\"required key is not set\", \"input_key\", key)\n\t\t\tfailed = true\n\t\t}\n\t}\n\tif failed {\n\t\treturn urfave.ErrSilent\n\t}\n\treturn nil\n}\n\nfunc readReusableWorkflow(fs afero.Fs, p string, wf *ReusableWorkflow) error {\n\tf, err := fs.Open(p)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"open a workflow file: %w\", err)\n\t}\n\tdefer f.Close()\n\tif err := yaml.NewDecoder(f).Decode(wf); err != nil {\n\t\terr := fmt.Errorf(\"parse a workflow file as YAML: %w\", err)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\treturn slogerr.With(err, //nolint:wrapcheck\n\t\t\t\t\"reference\", \"https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/codes/001.md\",\n\t\t\t)\n\t\t}\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (v *validateJob) read(ctx context.Context, wf *ReusableWorkflow) error { //nolint:cyclop\n\tif strings.HasPrefix(v.job.Uses, \"./\") {\n\t\t// local workflow\n\t\tif err := readReusableWorkflow(v.fs, v.job.Uses, wf); err != nil {\n\t\t\treturn fmt.Errorf(\"read a local workflow file: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\t// <owner>/<repo>[/<path>]@<ref>\n\tfullPath, ref, ok := strings.Cut(v.job.Uses, \"@\")\n\tif !ok {\n\t\treturn fmt.Errorf(\"invalid job.uses format: %s\", v.job.Uses)\n\t}\n\telems := strings.Split(fullPath, \"/\")\n\towner := elems[0]\n\trepo := elems[1]\n\tpath := strings.Join(elems[2:], \"/\")\n\tsha := ref\n\tif !fullCommitSHAPattern.MatchString(ref) {\n\t\t// Get SHA of actions\n\t\ts, _, err := v.gh.GetCommitSHA1(ctx, owner, repo, ref, \"\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get commit SHA1: %w\", err)\n\t\t}\n\t\tsha = s\n\t}\n\t// Download actions and store them in $GHALINT_ROOT_DIR/actions\n\t// Check if the action file exists\n\tcachePath := filepath.Join(v.rootDir, \"actions\", owner, repo, sha, path)\n\tif f, err := afero.Exists(v.fs, cachePath); err != nil {\n\t\treturn fmt.Errorf(\"check if the workflow file exists: %w\", err)\n\t} else if f {\n\t\tif err := readReusableWorkflow(v.fs, cachePath, wf); err != nil {\n\t\t\treturn fmt.Errorf(\"read a cached workflow file: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\t// Download a wofklow file\n\tcontent, _, _, err := v.gh.GetContents(ctx, owner, repo, path, &github.RepositoryContentGetOptions{\n\t\tRef: sha,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"download workflow file: %w\", err)\n\t}\n\t// write workflow to the cache dir\n\tif err := v.fs.MkdirAll(filepath.Dir(cachePath), dirPermission); err != nil {\n\t\treturn fmt.Errorf(\"create workflow directory: %w\", err)\n\t}\n\tc, err := content.GetContent()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get content: %w\", err)\n\t}\n\tb := []byte(c)\n\tif err := afero.WriteFile(v.fs, cachePath, b, filePermission); err != nil {\n\t\treturn fmt.Errorf(\"write workflow file: %w\", err)\n\t}\n\tif err := yaml.Unmarshal(b, wf); err != nil {\n\t\treturn fmt.Errorf(\"unmarshal workflow file: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/controller/schema/run.go",
    "content": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave\"\n)\n\nfunc (c *Controller) Run(ctx context.Context) error {\n\t// Find action.yaml and workflow files\n\tfailed := false\n\tif err := c.runWorkflow(ctx); err != nil {\n\t\tfailed = true\n\t\tif !errors.Is(err, urfave.ErrSilent) {\n\t\t\tslogerr.WithError(c.logger, err).Error(\"validate workflows\")\n\t\t}\n\t}\n\tif err := c.runActions(ctx); err != nil {\n\t\tif !errors.Is(err, urfave.ErrSilent) {\n\t\t\treturn fmt.Errorf(\"validate actions: %w\", err)\n\t\t}\n\t\treturn urfave.ErrSilent\n\t}\n\tif failed {\n\t\treturn urfave.ErrSilent\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/controller/schema/step.go",
    "content": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/github\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave\"\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype validateStep struct {\n\tstep    *workflow.Step\n\tlogger  *slog.Logger\n\tfs      afero.Fs\n\tgh      GitHub\n\trootDir string\n}\n\nvar fullCommitSHAPattern = regexp.MustCompile(`\\b[0-9a-f]{40}\\b`)\n\nfunc (v *validateStep) readAction(ctx context.Context, action *workflow.Action) error { //nolint:cyclop\n\tif strings.HasPrefix(v.step.Uses, \"./\") {\n\t\t// local action\n\t\tif err := v.readLocalAction(action); err != nil {\n\t\t\treturn fmt.Errorf(\"read a local action file: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\t// <owner>/<repo>[/<path>]@<ref>\n\tfullPath, ref, ok := strings.Cut(v.step.Uses, \"@\")\n\tif !ok {\n\t\treturn fmt.Errorf(\"invalid action format: %s\", v.step.Uses)\n\t}\n\telems := strings.Split(fullPath, \"/\")\n\towner := elems[0]\n\trepo := elems[1]\n\tpath := strings.Join(elems[2:], \"/\")\n\tsha := ref\n\tif !fullCommitSHAPattern.MatchString(ref) {\n\t\t// Get SHA of actions\n\t\ts, _, err := v.gh.GetCommitSHA1(ctx, owner, repo, ref, \"\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"get commit SHA1: %w\", err)\n\t\t}\n\t\tsha = s\n\t}\n\t// Download actions and store them in $GHALINT_ROOT_DIR/actions\n\t// Check if the action file exists\n\tcachePath := filepath.Join(v.rootDir, \"actions\", owner, repo, sha, path, \"action.yaml\")\n\tif f, err := afero.Exists(v.fs, cachePath); err != nil {\n\t\treturn fmt.Errorf(\"check if the action file exists: %w\", err)\n\t} else if f {\n\t\tif err := workflow.ReadAction(v.fs, cachePath, action); err != nil {\n\t\t\treturn fmt.Errorf(\"read a cached action file: %w\", err)\n\t\t}\n\t\treturn nil\n\t}\n\t// Download action.yaml or action.yml\n\tcontent, err := v.download(ctx, &downloadInput{\n\t\tOwner: owner,\n\t\tRepo:  repo,\n\t\tPath:  path,\n\t\tRef:   sha,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"download action file: %w\", err)\n\t}\n\t// write action.yaml to $GHALINT_ROOT_DIR/actions/<owner>/<repo>/<path>\n\tif err := v.fs.MkdirAll(filepath.Dir(cachePath), dirPermission); err != nil {\n\t\treturn fmt.Errorf(\"create action directory: %w\", err)\n\t}\n\tif err := afero.WriteFile(v.fs, cachePath, []byte(content), filePermission); err != nil {\n\t\treturn fmt.Errorf(\"write action file: %w\", err)\n\t}\n\tif err := yaml.Unmarshal([]byte(content), action); err != nil {\n\t\treturn fmt.Errorf(\"unmarshal action file: %w\", err)\n\t}\n\treturn nil\n}\n\nconst (\n\tfilePermission = 0o644\n\tdirPermission  = 0o755\n)\n\ntype downloadInput struct {\n\tOwner string\n\tRepo  string\n\tPath  string\n\tRef   string\n}\n\nfunc (v *validateStep) download(ctx context.Context, input *downloadInput) (string, error) {\n\tfor _, file := range []string{\"action.yaml\", \"action.yml\"} {\n\t\tcontent, _, _, err := v.gh.GetContents(ctx, input.Owner, input.Repo, filepath.Join(input.Path, file), &github.RepositoryContentGetOptions{\n\t\t\tRef: input.Ref,\n\t\t})\n\t\tif err != nil {\n\t\t\tslogerr.WithError(v.logger, err).Debug(\"get action file\")\n\t\t\tcontinue\n\t\t}\n\t\ts, err := content.GetContent()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"get content: %w\", err)\n\t\t}\n\t\treturn s, nil\n\t}\n\treturn \"\", errors.New(\"action file can't be downloaded\")\n}\n\nfunc (v *validateStep) validate(ctx context.Context) error {\n\t// Validate inputs\n\tif v.step.Uses == \"\" {\n\t\treturn nil\n\t}\n\tv.logger = v.logger.With(\"action\", v.step.Uses)\n\taction := &workflow.Action{}\n\tif err := v.readAction(ctx, action); err != nil {\n\t\treturn fmt.Errorf(\"read action: %w\", err)\n\t}\n\tvalidKeys := map[string]struct{}{}\n\trequiredKeys := map[string]struct{}{}\n\tvalidKeysArray := make([]string, 0, len(action.Inputs))\n\trequiredKeysArray := []string{}\n\tfor key, input := range action.Inputs {\n\t\tvalidKeysArray = append(validKeysArray, key)\n\t\tvalidKeys[key] = struct{}{}\n\t\tif input.Required {\n\t\t\trequiredKeys[key] = struct{}{}\n\t\t\trequiredKeysArray = append(requiredKeysArray, key)\n\t\t}\n\t}\n\tvalidKeysS := strings.Join(validKeysArray, \", \")\n\trequiredKeysS := strings.Join(requiredKeysArray, \", \")\n\tv.logger = v.logger.With(\n\t\t\"valid_inputs\", validKeysS,\n\t\t\"required_inputs\", requiredKeysS,\n\t)\n\tfailed := false\n\t// Check if the input is valid\n\tfor key := range v.step.With {\n\t\tif _, ok := action.Inputs[key]; !ok {\n\t\t\tv.logger.Error(\"invalid input key\", \"input_key\", key)\n\t\t\tfailed = true\n\t\t}\n\t}\n\t// Check if required keys are set\n\tfor key := range requiredKeys {\n\t\tif _, ok := v.step.With[key]; !ok {\n\t\t\tv.logger.Error(\"required key is not set\", \"input_key\", key)\n\t\t\tfailed = true\n\t\t}\n\t}\n\tif failed {\n\t\treturn urfave.ErrSilent\n\t}\n\treturn nil\n}\n\nfunc (v *validateStep) readLocalAction(action *workflow.Action) error {\n\tfound := false\n\tfor _, file := range []string{\"action.yaml\", \"action.yml\"} {\n\t\tp := filepath.Join(v.step.Uses, file)\n\t\tif f, err := afero.Exists(v.fs, p); err != nil {\n\t\t\treturn fmt.Errorf(\"check if the action file exists: %w\", err)\n\t\t} else if !f {\n\t\t\tcontinue\n\t\t}\n\t\tfound = true\n\t\tif err := workflow.ReadAction(v.fs, p, action); err != nil {\n\t\t\treturn fmt.Errorf(\"read a local action file: %w\", err)\n\t\t}\n\t}\n\tif !found {\n\t\treturn errors.New(\"local action file not found\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/controller/schema/workflow.go",
    "content": "package schema\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/urfave\"\n)\n\nfunc (c *Controller) runWorkflow(ctx context.Context) error {\n\tfilePaths, err := workflow.List(c.fs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"find workflow files: %w\", err)\n\t}\n\tfailed := false\n\tfor _, filePath := range filePaths {\n\t\tlogger := c.logger.With(\"workflow_file_path\", filePath)\n\t\tvw := &validateWorkflow{\n\t\t\tworkflow: filePath,\n\t\t\tlogger:   logger,\n\t\t\tfs:       c.fs,\n\t\t\tgh:       c.gh,\n\t\t\trootDir:  c.rootDir,\n\t\t}\n\t\tif err := vw.validate(ctx); err != nil {\n\t\t\tfailed = true\n\t\t\tif !errors.Is(err, urfave.ErrSilent) {\n\t\t\t\tslogerr.WithError(logger, err).Error(\"validate workflow\")\n\t\t\t}\n\t\t}\n\t}\n\tif failed {\n\t\treturn urfave.ErrSilent\n\t}\n\treturn nil\n}\n\ntype validateWorkflow struct {\n\tworkflow string\n\tlogger   *slog.Logger\n\tfs       afero.Fs\n\tgh       GitHub\n\trootDir  string\n}\n\nfunc (v *validateWorkflow) validate(ctx context.Context) error {\n\twf := &workflow.Workflow{\n\t\tFilePath: v.workflow,\n\t}\n\tif err := workflow.Read(v.fs, v.workflow, wf); err != nil {\n\t\treturn fmt.Errorf(\"read a workflow file: %w\", err)\n\t}\n\tfailed := false\n\tfor name, job := range wf.Jobs {\n\t\tvj := &validateJob{\n\t\t\tjob:     job,\n\t\t\tlogger:  v.logger.With(\"job_key\", name),\n\t\t\tfs:      v.fs,\n\t\t\tgh:      v.gh,\n\t\t\trootDir: v.rootDir,\n\t\t}\n\t\tif err := vj.validate(ctx); err != nil {\n\t\t\tfailed = true\n\t\t\tif !errors.Is(err, urfave.ErrSilent) {\n\t\t\t\tslogerr.WithError(v.logger, err).Error(\"validate job\")\n\t\t\t}\n\t\t}\n\t}\n\tif failed {\n\t\treturn urfave.ErrSilent\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/github/github.go",
    "content": "package github\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/google/go-github/v86/github\"\n\t\"github.com/suzuki-shunsuke/urfave-cli-v3-util/keyring/ghtoken\"\n\t\"golang.org/x/oauth2\"\n)\n\ntype (\n\tListOptions                 = github.ListOptions\n\tReference                   = github.Reference\n\tResponse                    = github.Response\n\tRepositoryTag               = github.RepositoryTag\n\tRepositoryRelease           = github.RepositoryRelease\n\tClient                      = github.Client\n\tGitObject                   = github.GitObject\n\tCommit                      = github.Commit\n\tRepositoryContentGetOptions = github.RepositoryContentGetOptions\n\tRepositoryContent           = github.RepositoryContent\n)\n\nfunc New(ctx context.Context, logger *slog.Logger) *Client {\n\treturn github.NewClient(getHTTPClientForGitHub(ctx, logger, getGitHubToken()))\n}\n\nfunc getGitHubToken() string {\n\treturn os.Getenv(\"GITHUB_TOKEN\")\n}\n\nfunc checkKeyringEnabled() bool {\n\treturn os.Getenv(\"GHALINT_KEYRING_ENABLED\") == \"true\"\n}\n\nfunc getHTTPClientForGitHub(ctx context.Context, logger *slog.Logger, token string) *http.Client {\n\tif token == \"\" {\n\t\tif checkKeyringEnabled() {\n\t\t\treturn oauth2.NewClient(ctx, ghtoken.NewTokenSource(logger, KeyService))\n\t\t}\n\t\treturn http.DefaultClient\n\t}\n\treturn oauth2.NewClient(ctx, oauth2.StaticTokenSource(\n\t\t&oauth2.Token{AccessToken: token},\n\t))\n}\n"
  },
  {
    "path": "pkg/github/keyring.go",
    "content": "package github\n\nconst (\n\tKeyService = \"suzuki-shunsuke/ghalint\"\n)\n"
  },
  {
    "path": "pkg/policy/action_ref_should_be_full_length_commit_sha_policy.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"path\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n)\n\ntype ActionRefShouldBeSHAPolicy struct {\n\tsha1Pattern   *regexp.Regexp\n\tsha256Pattern *regexp.Regexp\n}\n\nfunc NewActionRefShouldBeSHAPolicy() *ActionRefShouldBeSHAPolicy {\n\treturn &ActionRefShouldBeSHAPolicy{\n\t\tsha1Pattern:   regexp.MustCompile(`\\b[0-9a-f]{40}\\b`),\n\t\tsha256Pattern: regexp.MustCompile(`\\b[0-9a-f]{64}\\b`),\n\t}\n}\n\nfunc (p *ActionRefShouldBeSHAPolicy) Name() string {\n\treturn \"action_ref_should_be_full_length_commit_sha\"\n}\n\nfunc (p *ActionRefShouldBeSHAPolicy) ID() string {\n\treturn \"008\"\n}\n\nfunc (p *ActionRefShouldBeSHAPolicy) ApplyJob(_ *slog.Logger, cfg *config.Config, _ *JobContext, job *workflow.Job) error {\n\treturn p.apply(cfg, job.Uses)\n}\n\nfunc (p *ActionRefShouldBeSHAPolicy) ApplyStep(_ *slog.Logger, cfg *config.Config, _ *StepContext, step *workflow.Step) error {\n\treturn p.apply(cfg, step.Uses)\n}\n\nfunc (p *ActionRefShouldBeSHAPolicy) apply(cfg *config.Config, uses string) error {\n\taction := p.checkUses(uses)\n\tif action == \"\" || p.excluded(action, cfg.Excludes) {\n\t\treturn nil\n\t}\n\treturn slogerr.With(errors.New(\"action ref should be full length SHA\"), //nolint:wrapcheck\n\t\t\"action\", action,\n\t)\n}\n\nfunc (p *ActionRefShouldBeSHAPolicy) checkUses(uses string) string {\n\tif uses == \"\" {\n\t\treturn \"\"\n\t}\n\tif ref, ok := strings.CutPrefix(uses, \"docker://\"); ok {\n\t\trepoAndTag, digest, hasDigest := strings.Cut(ref, \"@sha256:\")\n\t\tif hasDigest && p.sha256Pattern.MatchString(digest) {\n\t\t\treturn \"\"\n\t\t}\n\t\trepo := repoAndTag\n\t\tlastColon := strings.LastIndex(repoAndTag, \":\")\n\t\tlastSlash := strings.LastIndex(repoAndTag, \"/\")\n\t\tif lastColon != -1 && lastColon > lastSlash {\n\t\t\trepo = repoAndTag[:lastColon]\n\t\t}\n\t\treturn \"docker://\" + repo\n\t}\n\taction, tag, ok := strings.Cut(uses, \"@\")\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tif p.sha1Pattern.MatchString(tag) {\n\t\treturn \"\"\n\t}\n\treturn action\n}\n\nfunc (p *ActionRefShouldBeSHAPolicy) excluded(action string, excludes []*config.Exclude) bool {\n\tfor _, exclude := range excludes {\n\t\tif exclude.PolicyName != p.Name() {\n\t\t\tcontinue\n\t\t}\n\t\tif f, _ := path.Match(exclude.ActionName, action); f {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "pkg/policy/action_ref_should_be_full_length_commit_sha_policy_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestActionRefShouldBeSHAPolicy_ApplyJob(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname  string\n\t\tcfg   *config.Config\n\t\tjob   *workflow.Job\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"exclude\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"slsa-framework/slsa-github-generator\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{\n\t\t\t\t\t\tUses: \"slsa-framework/slsa-github-generator@v1.5.0\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"job error\",\n\t\t\tisErr: true,\n\t\t\tcfg:   &config.Config{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tUses: \"suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@v0.4.4\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"docker image with digest\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tUses: \"docker://rhysd/actionlint:1.7.7@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"docker image with digest (no tag)\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tUses: \"docker://rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"docker image with port and digest\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tUses: \"docker://registry.example.com:5000/myimage:1.0.0@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"docker image with tag\",\n\t\t\tisErr: true,\n\t\t\tcfg:   &config.Config{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tUses: \"docker://rhysd/actionlint:latest\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"docker image with port and tag\",\n\t\t\tisErr: true,\n\t\t\tcfg:   &config.Config{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tUses: \"docker://registry.example.com:5000/myimage:latest\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude docker image with tag\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"docker://rhysd/actionlint\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tUses: \"docker://rhysd/actionlint:latest\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude docker image with port and tag\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"docker://registry.example.com:5000/myimage\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tUses: \"docker://registry.example.com:5000/myimage:latest\",\n\t\t\t},\n\t\t},\n\t}\n\tp := policy.NewActionRefShouldBeSHAPolicy()\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyJob(logger, d.cfg, nil, d.job); err != nil {\n\t\t\t\tif d.isErr {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestActionRefShouldBeSHAPolicy_ApplyStep(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname  string\n\t\tcfg   *config.Config\n\t\tstep  *workflow.Step\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"exclude\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"slsa-framework/slsa-github-generator\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"slsa-framework/slsa-github-generator@v1.5.0\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude with glob pattern\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"slsa-framework/*\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"slsa-framework/slsa-github-generator@v1.5.0\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"step error\",\n\t\t\tisErr: true,\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"actions/checkout\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"slsa-framework/slsa-github-generator@v1.5.0\",\n\t\t\t\tID:   \"generate\",\n\t\t\t\tName: \"Generate SLSA Provenance\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"docker image with digest\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"docker://rhysd/actionlint:1.7.7@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"docker image with digest (no tag)\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"docker://rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"docker image with port and digest\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"docker://registry.example.com:5000/myimage:1.0.0@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"docker image with tag\",\n\t\t\tisErr: true,\n\t\t\tcfg:   &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"docker://rhysd/actionlint:latest\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"docker image with port and tag\",\n\t\t\tisErr: true,\n\t\t\tcfg:   &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"docker://registry.example.com:5000/myimage:latest\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude docker image with tag\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"docker://rhysd/actionlint\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"docker://rhysd/actionlint:latest\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude docker image with port and tag\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"action_ref_should_be_full_length_commit_sha\",\n\t\t\t\t\t\tActionName: \"docker://registry.example.com:5000/myimage\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"docker://registry.example.com:5000/myimage:latest\",\n\t\t\t},\n\t\t},\n\t}\n\tp := policy.NewActionRefShouldBeSHAPolicy()\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyStep(logger, d.cfg, nil, d.step); err != nil {\n\t\t\t\tif d.isErr {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/action_shell_is_required.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype ActionShellIsRequiredPolicy struct{}\n\nfunc (p *ActionShellIsRequiredPolicy) Name() string {\n\treturn \"action_shell_is_required\"\n}\n\nfunc (p *ActionShellIsRequiredPolicy) ID() string {\n\treturn \"011\"\n}\n\nfunc (p *ActionShellIsRequiredPolicy) ApplyStep(_ *slog.Logger, _ *config.Config, _ *StepContext, step *workflow.Step) error {\n\tif step.Run != \"\" && step.Shell == \"\" {\n\t\treturn errors.New(\"shell is required if run is set\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/policy/action_shell_is_required_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestActionShellIsRequiredPolicy_ApplyStep(t *testing.T) {\n\tt.Parallel()\n\tdata := []struct {\n\t\tname  string\n\t\tstep  *workflow.Step\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"pass\",\n\t\t\tstep: &workflow.Step{\n\t\t\t\tRun:   \"echo hello\",\n\t\t\t\tShell: \"bash\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"step error\",\n\t\t\tisErr: true,\n\t\t\tstep: &workflow.Step{\n\t\t\t\tRun: \"echo hello\",\n\t\t\t},\n\t\t},\n\t}\n\tp := &policy.ActionShellIsRequiredPolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyStep(logger, nil, nil, d.step); err != nil {\n\t\t\t\tif d.isErr {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/checkout_persist_credentials_should_be_false.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype CheckoutPersistCredentialShouldBeFalsePolicy struct{}\n\nfunc (p *CheckoutPersistCredentialShouldBeFalsePolicy) Name() string {\n\treturn \"checkout_persist_credentials_should_be_false\"\n}\n\nfunc (p *CheckoutPersistCredentialShouldBeFalsePolicy) ID() string {\n\treturn \"013\"\n}\n\nfunc (p *CheckoutPersistCredentialShouldBeFalsePolicy) ApplyStep(_ *slog.Logger, cfg *config.Config, stepCtx *StepContext, step *workflow.Step) error {\n\tif p.excluded(stepCtx, cfg.Excludes) {\n\t\treturn nil\n\t}\n\tif !strings.HasPrefix(step.Uses, \"actions/checkout@\") {\n\t\treturn nil\n\t}\n\tf, ok := step.With[\"persist-credentials\"]\n\tif !ok {\n\t\treturn errors.New(\"persist-credentials should be false\")\n\t}\n\tif f != \"false\" {\n\t\treturn errors.New(\"persist-credentials should be false\")\n\t}\n\treturn nil\n}\n\nfunc (p *CheckoutPersistCredentialShouldBeFalsePolicy) excluded(stepCtx *StepContext, excludes []*config.Exclude) bool {\n\tfor _, exclude := range excludes {\n\t\tif exclude.PolicyName != p.Name() {\n\t\t\tcontinue\n\t\t}\n\t\tif stepCtx.Action != nil {\n\t\t\tif exclude.ActionFilePath != stepCtx.FilePath {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\t\tif exclude.JobName != stepCtx.Job.Name {\n\t\t\tcontinue\n\t\t}\n\t\tif exclude.WorkflowFilePath != stepCtx.Job.Workflow.FilePath {\n\t\t\tcontinue\n\t\t}\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "pkg/policy/checkout_persist_credentials_should_be_false_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestCheckoutPersistCredentialShouldBeFalsePolicy_ApplyStep(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname    string\n\t\tcfg     *config.Config\n\t\tstep    *workflow.Step\n\t\tstepCtx *policy.StepContext\n\t\tisErr   bool\n\t}{\n\t\t{\n\t\t\tname: \"exclude\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName:       \"checkout_persist_credentials_should_be_false\",\n\t\t\t\t\t\tWorkflowFilePath: \".github/workflows/test.yml\",\n\t\t\t\t\t\tJobName:          \"test\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstepCtx: &policy.StepContext{\n\t\t\t\tJob: &policy.JobContext{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\t\tFilePath: \".github/workflows/test.yml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/checkout@v4\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"persist-credentials is not set\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName:       \"checkout_persist_credentials_should_be_false\",\n\t\t\t\t\t\tJobName:          \"test-2\",\n\t\t\t\t\t\tWorkflowFilePath: \".github/workflows/test.yml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstepCtx: &policy.StepContext{\n\t\t\t\tJob: &policy.JobContext{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\t\tFilePath: \".github/workflows/test.yml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/checkout@v4\",\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"persist-credentials is true\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"checkout_persist_credentials_should_be_false\",\n\t\t\t\t\t\tJobName:    \"test-2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstepCtx: &policy.StepContext{\n\t\t\t\tJob: &policy.JobContext{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\t\tFilePath: \".github/workflows/test.yml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/checkout@v4\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"persist-credentials\": \"true\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"persist-credentials is false\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName: \"checkout_persist_credentials_should_be_false\",\n\t\t\t\t\t\tJobName:    \"test-2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstepCtx: &policy.StepContext{\n\t\t\t\tJob: &policy.JobContext{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\t\tFilePath: \".github/workflows/test.yml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/checkout@v4\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"persist-credentials\": \"false\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"not checkout\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{},\n\t\t\t},\n\t\t\tstepCtx: &policy.StepContext{\n\t\t\t\tJob: &policy.JobContext{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\t\tFilePath: \".github/workflows/test.yml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/cache@v4\",\n\t\t\t},\n\t\t},\n\t}\n\tp := &policy.CheckoutPersistCredentialShouldBeFalsePolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyStep(logger, d.cfg, d.stepCtx, d.step); err != nil {\n\t\t\t\tif d.isErr {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/context.go",
    "content": "package policy\n\nimport \"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\ntype WorkflowContext struct {\n\tFilePath string\n\tWorkflow *workflow.Workflow\n}\n\ntype JobContext struct {\n\tName     string\n\tWorkflow *WorkflowContext\n\tJob      *workflow.Job\n}\n\ntype StepContext struct {\n\tFilePath string\n\tAction   *workflow.Action\n\tJob      *JobContext\n}\n"
  },
  {
    "path": "pkg/policy/deny_inherit_secrets.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype DenyInheritSecretsPolicy struct{}\n\nfunc (p *DenyInheritSecretsPolicy) Name() string {\n\treturn \"deny_inherit_secrets\"\n}\n\nfunc (p *DenyInheritSecretsPolicy) ID() string {\n\treturn \"004\"\n}\n\nfunc (p *DenyInheritSecretsPolicy) ApplyJob(_ *slog.Logger, cfg *config.Config, jobCtx *JobContext, job *workflow.Job) error {\n\tif checkExcludes(p.Name(), jobCtx, cfg) {\n\t\treturn nil\n\t}\n\tif job.Secrets.Inherit() {\n\t\treturn errors.New(\"`secrets: inherit` should not be used. Only required secrets should be passed explicitly\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/policy/deny_inherit_secrets_test.go",
    "content": "//nolint:funlen\npackage policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestDenyInheritSecretsPolicy_ApplyJob(t *testing.T) {\n\tt.Parallel()\n\tdata := []struct {\n\t\tname   string\n\t\tjob    string\n\t\tcfg    *config.Config\n\t\tjobCtx *policy.JobContext\n\t\tisErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"exclude\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName:       \"deny_inherit_secrets\",\n\t\t\t\t\t\tWorkflowFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t\t\tJobName:          \"foo\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t},\n\t\t\t\tName: \"foo\",\n\t\t\t},\n\t\t\tjob: `secrets: inherit`,\n\t\t},\n\t\t{\n\t\t\tname: \"not exclude\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName:       \"deny_inherit_secrets\",\n\t\t\t\t\t\tWorkflowFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t\t\tJobName:          \"bar\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t},\n\t\t\t\tName: \"foo\",\n\t\t\t},\n\t\t\tjob:   `secrets: inherit`,\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"error\",\n\t\t\tjob:  `secrets: inherit`,\n\t\t\tcfg:  &config.Config{},\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t},\n\t\t\t\tName: \"foo\",\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"pass\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t},\n\t\t\t\tName: \"foo\",\n\t\t\t},\n\t\t\tjob: `secrets:\n      foo: ${{secrets.API_KEY}}`,\n\t\t},\n\t}\n\tp := &policy.DenyInheritSecretsPolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tjob := &workflow.Job{}\n\t\t\tif err := yaml.Unmarshal([]byte(d.job), job); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif err := p.ApplyJob(logger, d.cfg, d.jobCtx, job); err != nil {\n\t\t\t\tif d.isErr {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/deny_job_container_latest_image.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype DenyJobContainerLatestImagePolicy struct{}\n\nfunc (p *DenyJobContainerLatestImagePolicy) Name() string {\n\treturn \"deny_job_container_latest_image\"\n}\n\nfunc (p *DenyJobContainerLatestImagePolicy) ID() string {\n\treturn \"007\"\n}\n\nfunc (p *DenyJobContainerLatestImagePolicy) ApplyJob(logger *slog.Logger, _ *config.Config, _ *JobContext, job *workflow.Job) error {\n\tif job.Container == nil {\n\t\treturn nil\n\t}\n\tif job.Container.Image == \"\" {\n\t\treturn errors.New(\"job container should have image\")\n\t}\n\tif strings.Contains(job.Container.Image, \"${{\") {\n\t\tlogger.Debug(\"job container image contains `${{`; skipping latest image check\")\n\t\treturn nil\n\t}\n\t_, tag, ok := strings.Cut(job.Container.Image, \":\")\n\tif !ok {\n\t\treturn errors.New(\"job container image should be <image name>:<tag>\")\n\t}\n\tif tag == \"latest\" {\n\t\treturn errors.New(\"job container image tag should not be `latest`\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/policy/deny_job_container_latest_image_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestDenyJobContainerLatestImagePolicy_ApplyJob(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname  string\n\t\tjob   *workflow.Job\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"pass\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tContainer: &workflow.Container{\n\t\t\t\t\tImage: \"node:18\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"job container should have image\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tContainer: &workflow.Container{},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"job container image should have tag\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tContainer: &workflow.Container{\n\t\t\t\t\tImage: \"node\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"latest\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tContainer: &workflow.Container{\n\t\t\t\t\tImage: \"node:latest\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"Use variables\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tContainer: &workflow.Container{\n\t\t\t\t\tImage: \"mirror.gcr.io/${{needs.list.outputs.image}}\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: false,\n\t\t},\n\t}\n\tp := &policy.DenyJobContainerLatestImagePolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyJob(logger, nil, nil, d.job); err != nil {\n\t\t\t\tif !d.isErr {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/deny_read_all_policy.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype DenyReadAllPermissionPolicy struct{}\n\nfunc (p *DenyReadAllPermissionPolicy) Name() string {\n\treturn \"deny_read_all_permission\"\n}\n\nfunc (p *DenyReadAllPermissionPolicy) ID() string {\n\treturn \"002\"\n}\n\nfunc (p *DenyReadAllPermissionPolicy) ApplyJob(_ *slog.Logger, _ *config.Config, jobCtx *JobContext, job *workflow.Job) error {\n\twfReadAll := jobCtx.Workflow.Workflow.Permissions.ReadAll()\n\tif job.Permissions.ReadAll() {\n\t\treturn errors.New(\"don't use read-all permission\")\n\t}\n\tif job.Permissions.IsNil() && wfReadAll {\n\t\treturn errors.New(\"don't use read-all permission\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/policy/deny_read_all_policy_test.go",
    "content": "package policy_test //nolint:dupl\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestDenyReadAllPermissionPolicy_ApplyJob(t *testing.T) {\n\tt.Parallel()\n\tdata := []struct {\n\t\tname   string\n\t\tjobCtx *policy.JobContext\n\t\tjob    *workflow.Job\n\t\tisErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"don't use read-all\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tPermissions: workflow.NewPermissions(true, false, nil),\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"job permissions is null and workflow permissions is read-all\",\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tWorkflow: &workflow.Workflow{\n\t\t\t\t\t\tPermissions: workflow.NewPermissions(true, false, nil),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjob:   &workflow.Job{},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"pass\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tPermissions: workflow.NewPermissions(false, false, map[string]string{\n\t\t\t\t\t\"contents\": \"read\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t}\n\tp := &policy.DenyReadAllPermissionPolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tif d.jobCtx == nil {\n\t\t\td.jobCtx = &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tWorkflow: &workflow.Workflow{},\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyJob(logger, nil, d.jobCtx, d.job); err != nil {\n\t\t\t\tif !d.isErr {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/deny_write_all_policy.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype DenyWriteAllPermissionPolicy struct{}\n\nfunc (p *DenyWriteAllPermissionPolicy) Name() string {\n\treturn \"deny_write_all_permission\"\n}\n\nfunc (p *DenyWriteAllPermissionPolicy) ID() string {\n\treturn \"003\"\n}\n\nfunc (p *DenyWriteAllPermissionPolicy) ApplyJob(_ *slog.Logger, _ *config.Config, jobCtx *JobContext, job *workflow.Job) error {\n\twfWriteAll := jobCtx.Workflow.Workflow.Permissions.WriteAll()\n\tif job.Permissions.WriteAll() {\n\t\treturn errors.New(\"don't use write-all permission\")\n\t}\n\tif job.Permissions.IsNil() && wfWriteAll {\n\t\treturn errors.New(\"don't use write-all permission\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/policy/deny_write_all_policy_test.go",
    "content": "package policy_test //nolint:dupl\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestDenyWriteAllPermissionPolicy_ApplyJob(t *testing.T) {\n\tt.Parallel()\n\tdata := []struct {\n\t\tname   string\n\t\tjobCtx *policy.JobContext\n\t\tjob    *workflow.Job\n\t\tisErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"don't use write-all\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tPermissions: workflow.NewPermissions(false, true, nil),\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"job permissions is null and workflow permissions is write-all\",\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tWorkflow: &workflow.Workflow{\n\t\t\t\t\t\tPermissions: workflow.NewPermissions(false, true, nil),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjob:   &workflow.Job{},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"pass\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tPermissions: workflow.NewPermissions(false, false, map[string]string{\n\t\t\t\t\t\"contents\": \"write\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t}\n\tp := &policy.DenyWriteAllPermissionPolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tif d.jobCtx == nil {\n\t\t\td.jobCtx = &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tWorkflow: &workflow.Workflow{},\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyJob(logger, nil, d.jobCtx, d.job); err != nil {\n\t\t\t\tif !d.isErr {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/error.go",
    "content": "package policy\n\nimport \"errors\"\n\nvar (\n\terrPermissionHyphenIsRequired = errors.New(\"an input `permission-*` is required\")\n\terrPermissionsIsRequired      = errors.New(\"the input `permissions` is required\")\n\terrRepositoriesIsRequired     = errors.New(\"the input `repositories` is required\")\n\terrEmpty                      = errors.New(\"\")\n)\n"
  },
  {
    "path": "pkg/policy/github_app_should_limit_permissions.go",
    "content": "package policy\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n)\n\ntype GitHubAppShouldLimitPermissionsPolicy struct{}\n\nfunc (p *GitHubAppShouldLimitPermissionsPolicy) Name() string {\n\treturn \"github_app_should_limit_permissions\"\n}\n\nfunc (p *GitHubAppShouldLimitPermissionsPolicy) ID() string {\n\treturn \"010\"\n}\n\nfunc (p *GitHubAppShouldLimitPermissionsPolicy) ApplyStep(_ *slog.Logger, _ *config.Config, _ *StepContext, step *workflow.Step) (ge error) { //nolint:cyclop\n\taction := p.checkUses(step.Uses)\n\tif action == \"\" {\n\t\treturn nil\n\t}\n\tdefer func() {\n\t\tif ge != nil {\n\t\t\tge = slogerr.With(ge,\n\t\t\t\t\"action\", action,\n\t\t\t)\n\t\t}\n\t}()\n\n\tswitch action {\n\tcase \"tibdex/github-app-token\":\n\t\tif step.With == nil {\n\t\t\treturn errPermissionsIsRequired\n\t\t}\n\t\tif _, ok := step.With[\"permissions\"]; !ok {\n\t\t\treturn errPermissionsIsRequired\n\t\t}\n\tcase \"actions/create-github-app-token\":\n\t\tif step.With == nil {\n\t\t\treturn errPermissionsIsRequired\n\t\t}\n\t\terr := errPermissionHyphenIsRequired\n\t\tfor k := range step.With {\n\t\t\tif strings.HasPrefix(k, \"permission-\") {\n\t\t\t\terr = nil\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (p *GitHubAppShouldLimitPermissionsPolicy) checkUses(uses string) string {\n\tif uses == \"\" {\n\t\treturn \"\"\n\t}\n\taction, _, _ := strings.Cut(uses, \"@\")\n\treturn action\n}\n"
  },
  {
    "path": "pkg/policy/github_app_should_limit_permissions_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestGitHubAppShouldLimitPermissionsPolicy_ApplyStep(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname    string\n\t\tcfg     *config.Config\n\t\tstepCtx *policy.StepContext\n\t\tstep    *workflow.Step\n\t\tisErr   bool\n\t}{\n\t\t{\n\t\t\tname:  \"tibdex/github-app-token fail\",\n\t\t\tisErr: true,\n\t\t\tcfg:   &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"tibdex/github-app-token@v2\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app_id\":      \"xxx\",\n\t\t\t\t\t\"private_key\": \"xxx\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"tibdex/github-app-token success\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"tibdex/github-app-token@v2\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app_id\":      \"xxx\",\n\t\t\t\t\t\"private_key\": \"xxx\",\n\t\t\t\t\t\"permissions\": \"{}\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"actions/create-github-app-token fail\",\n\t\t\tisErr: true,\n\t\t\tcfg:   &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/create-github-app-token@v1.12.0\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app-id\":      \"xxx\",\n\t\t\t\t\t\"private-key\": \"xxx\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"actions/create-github-app-token succeed\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/create-github-app-token@v1.12.0\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app-id\":            \"xxx\",\n\t\t\t\t\t\"private-key\":       \"xxx\",\n\t\t\t\t\t\"permission-issues\": \"write\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tp := &policy.GitHubAppShouldLimitPermissionsPolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tif d.stepCtx == nil {\n\t\t\td.stepCtx = &policy.StepContext{\n\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\tJob: &policy.JobContext{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyStep(logger, d.cfg, d.stepCtx, d.step); err != nil {\n\t\t\t\tif d.isErr {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/github_app_should_limit_repositories.go",
    "content": "package policy\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n)\n\ntype GitHubAppShouldLimitRepositoriesPolicy struct{}\n\nfunc (p *GitHubAppShouldLimitRepositoriesPolicy) Name() string {\n\treturn \"github_app_should_limit_repositories\"\n}\n\nfunc (p *GitHubAppShouldLimitRepositoriesPolicy) ID() string {\n\treturn \"009\"\n}\n\nfunc (p *GitHubAppShouldLimitRepositoriesPolicy) ApplyStep(logger *slog.Logger, cfg *config.Config, stepCtx *StepContext, step *workflow.Step) (ge error) { //nolint:cyclop\n\taction := p.checkUses(step.Uses)\n\tif action == \"\" {\n\t\treturn nil\n\t}\n\tdefer func() {\n\t\tif ge != nil {\n\t\t\tge = slogerr.With(ge,\n\t\t\t\t\"action\", action,\n\t\t\t)\n\t\t}\n\t}()\n\tif p.excluded(cfg, stepCtx, step) {\n\t\tlogger.Debug(\"this step is ignored\")\n\t\treturn nil\n\t}\n\tif action == \"tibdex/github-app-token\" {\n\t\tif step.With == nil {\n\t\t\treturn errRepositoriesIsRequired\n\t\t}\n\t\tif _, ok := step.With[\"repositories\"]; !ok {\n\t\t\treturn errRepositoriesIsRequired\n\t\t}\n\t\treturn nil\n\t}\n\tif action == \"actions/create-github-app-token\" {\n\t\tif step.With == nil {\n\t\t\treturn errRepositoriesIsRequired\n\t\t}\n\t\tif _, ok := step.With[\"repositories\"]; ok {\n\t\t\treturn nil\n\t\t}\n\t\tif _, ok := step.With[\"owner\"]; ok {\n\t\t\treturn errRepositoriesIsRequired\n\t\t}\n\t\treturn nil\n\t}\n\treturn nil\n}\n\nfunc (p *GitHubAppShouldLimitRepositoriesPolicy) checkUses(uses string) string {\n\tif uses == \"\" {\n\t\treturn \"\"\n\t}\n\taction, _, _ := strings.Cut(uses, \"@\")\n\treturn action\n}\n\nfunc (p *GitHubAppShouldLimitRepositoriesPolicy) excluded(cfg *config.Config, stepCtx *StepContext, step *workflow.Step) bool {\n\tfor _, exclude := range cfg.Excludes {\n\t\tif exclude.PolicyName != p.Name() {\n\t\t\tcontinue\n\t\t}\n\t\tif exclude.FilePath() != stepCtx.FilePath {\n\t\t\tcontinue\n\t\t}\n\t\tif stepCtx.Job != nil && exclude.JobName != stepCtx.Job.Name {\n\t\t\tcontinue\n\t\t}\n\t\tif exclude.StepID != step.ID {\n\t\t\tcontinue\n\t\t}\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "pkg/policy/github_app_should_limit_repositories_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestGitHubAppShouldLimitRepositoriesPolicy_ApplyStep(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname    string\n\t\tcfg     *config.Config\n\t\tstepCtx *policy.StepContext\n\t\tstep    *workflow.Step\n\t\tisErr   bool\n\t}{\n\t\t{\n\t\t\tname:  \"tibdex/github-app-token fail\",\n\t\t\tisErr: true,\n\t\t\tcfg:   &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"tibdex/github-app-token@v2\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app_id\":      \"xxx\",\n\t\t\t\t\t\"private_key\": \"xxx\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"tibdex/github-app-token success\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"tibdex/github-app-token@v2\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app_id\":       \"xxx\",\n\t\t\t\t\t\"private_key\":  \"xxx\",\n\t\t\t\t\t\"repositories\": \"{}\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"actions/create-github-app-token fail\",\n\t\t\tisErr: true,\n\t\t\tcfg:   &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/create-github-app-token@v2\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app-id\":      \"xxx\",\n\t\t\t\t\t\"private-key\": \"xxx\",\n\t\t\t\t\t\"owner\":       \"xxx\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"actions/create-github-app-token success\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/create-github-app-token@v2\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app-id\":       \"xxx\",\n\t\t\t\t\t\"private-key\":  \"xxx\",\n\t\t\t\t\t\"owner\":        \"xxx\",\n\t\t\t\t\t\"repositories\": \"foo,bar\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"actions/create-github-app-token success no owner\",\n\t\t\tcfg:  &config.Config{},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"actions/create-github-app-token@v2\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app-id\":      \"xxx\",\n\t\t\t\t\t\"private-key\": \"xxx\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName:       \"github_app_should_limit_repositories\",\n\t\t\t\t\t\tWorkflowFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t\t\tJobName:          \"test\",\n\t\t\t\t\t\tStepID:           \"token\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstepCtx: &policy.StepContext{\n\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\tJob: &policy.JobContext{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"tibdex/github-app-token@v2\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app_id\":      \"xxx\",\n\t\t\t\t\t\"private_key\": \"xxx\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude action\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName:     \"github_app_should_limit_repositories\",\n\t\t\t\t\t\tActionFilePath: \"foo/action.yaml\",\n\t\t\t\t\t\tStepID:         \"token\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tstepCtx: &policy.StepContext{\n\t\t\t\tFilePath: \"foo/action.yaml\",\n\t\t\t},\n\t\t\tstep: &workflow.Step{\n\t\t\t\tUses: \"tibdex/github-app-token@v2\",\n\t\t\t\tID:   \"token\",\n\t\t\t\tWith: map[string]string{\n\t\t\t\t\t\"app_id\":      \"xxx\",\n\t\t\t\t\t\"private_key\": \"xxx\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tp := &policy.GitHubAppShouldLimitRepositoriesPolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tif d.stepCtx == nil {\n\t\t\td.stepCtx = &policy.StepContext{\n\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\tJob: &policy.JobContext{\n\t\t\t\t\tName: \"test\",\n\t\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyStep(logger, d.cfg, d.stepCtx, d.step); err != nil {\n\t\t\t\tif d.isErr {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/job_permissions_policy.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype JobPermissionsPolicy struct{}\n\nfunc (p *JobPermissionsPolicy) Name() string {\n\treturn \"job_permissions\"\n}\n\nfunc (p *JobPermissionsPolicy) ID() string {\n\treturn \"001\"\n}\n\nfunc (p *JobPermissionsPolicy) ApplyJob(_ *slog.Logger, _ *config.Config, jobCtx *JobContext, job *workflow.Job) error {\n\twf := jobCtx.Workflow.Workflow\n\twfPermissions := wf.Permissions.Permissions()\n\tif wfPermissions != nil && len(wfPermissions) == 0 {\n\t\t// workflow's permissions is `{}`\n\t\treturn nil\n\t}\n\tif len(wf.Jobs) < 2 && wfPermissions != nil {\n\t\t// workflow permissions is set and there is only one job\n\t\treturn nil\n\t}\n\tif job.Permissions.IsNil() {\n\t\treturn errors.New(\"job should have permissions\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/policy/job_permissions_policy_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestJobPermissionsPolicy_ApplyJob(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname   string\n\t\tjobCtx *policy.JobContext\n\t\tjob    *workflow.Job\n\t\tisErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"workflow permissions is empty\",\n\t\t\tjob:  &workflow.Job{},\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tWorkflow: &workflow.Workflow{\n\t\t\t\t\t\tPermissions: workflow.NewPermissions(false, false, map[string]string{}),\n\t\t\t\t\t\tJobs: map[string]*workflow.Job{\n\t\t\t\t\t\t\t\"foo\": {},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workflow has only one job\",\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tWorkflow: &workflow.Workflow{\n\t\t\t\t\t\tPermissions: workflow.NewPermissions(false, false, map[string]string{\n\t\t\t\t\t\t\t\"contents\": \"read\",\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tJobs: map[string]*workflow.Job{\n\t\t\t\t\t\t\t\"foo\": {},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjob: &workflow.Job{},\n\t\t},\n\t\t{\n\t\t\tname: \"job should have permissions\",\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tWorkflow: &workflow.Workflow{\n\t\t\t\t\t\tPermissions: &workflow.Permissions{},\n\t\t\t\t\t\tJobs: map[string]*workflow.Job{\n\t\t\t\t\t\t\t\"foo\": {},\n\t\t\t\t\t\t\t\"bar\": {},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjob:   &workflow.Job{},\n\t\t\tisErr: true,\n\t\t},\n\t}\n\tp := &policy.JobPermissionsPolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyJob(logger, nil, d.jobCtx, d.job); err != nil {\n\t\t\t\tif !d.isErr {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/job_secrets_policy.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"regexp\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n)\n\ntype JobSecretsPolicy struct {\n\tsecretPattern      *regexp.Regexp\n\tgithubTokenPattern *regexp.Regexp\n}\n\nfunc NewJobSecretsPolicy() *JobSecretsPolicy {\n\treturn &JobSecretsPolicy{\n\t\tsecretPattern:      regexp.MustCompile(`\\${{ *secrets\\.[^ ]+ *}}`),\n\t\tgithubTokenPattern: regexp.MustCompile(`\\${{ *github\\.token+ *}}`),\n\t}\n}\n\nfunc (p *JobSecretsPolicy) Name() string {\n\treturn \"job_secrets\"\n}\n\nfunc (p *JobSecretsPolicy) ID() string {\n\treturn \"006\"\n}\n\nfunc checkExcludes(policyName string, jobCtx *JobContext, cfg *config.Config) bool {\n\tfor _, exclude := range cfg.Excludes {\n\t\tif exclude.PolicyName == policyName && jobCtx.Workflow.FilePath == exclude.WorkflowFilePath && jobCtx.Name == exclude.JobName {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (p *JobSecretsPolicy) ApplyJob(_ *slog.Logger, cfg *config.Config, jobCtx *JobContext, job *workflow.Job) error {\n\tif checkExcludes(p.Name(), jobCtx, cfg) {\n\t\treturn nil\n\t}\n\tif len(job.Steps) < 2 { //nolint:mnd\n\t\treturn nil\n\t}\n\tfor envName, envValue := range job.Env {\n\t\tif p.secretPattern.MatchString(envValue) {\n\t\t\treturn slogerr.With(errors.New(\"secret should not be set to job's env\"), //nolint:wrapcheck\n\t\t\t\t\"env_name\", envName,\n\t\t\t)\n\t\t}\n\t\tif p.githubTokenPattern.MatchString(envValue) {\n\t\t\treturn slogerr.With(errors.New(\"github.token should not be set to job's env\"), //nolint:wrapcheck\n\t\t\t\t\"env_name\", envName,\n\t\t\t)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/policy/job_secrets_policy_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestJobSecretsPolicy_ApplyJob(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname   string\n\t\tcfg    *config.Config\n\t\tjobCtx *policy.JobContext\n\t\tjob    *workflow.Job\n\t\tisErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"exclude\",\n\t\t\tcfg: &config.Config{\n\t\t\t\tExcludes: []*config.Exclude{\n\t\t\t\t\t{\n\t\t\t\t\t\tPolicyName:       \"job_secrets\",\n\t\t\t\t\t\tWorkflowFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t\t\tJobName:          \"foo\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tjobCtx: &policy.JobContext{\n\t\t\t\tWorkflow: &policy.WorkflowContext{\n\t\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\t},\n\t\t\t\tName: \"foo\",\n\t\t\t},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tEnv: map[string]string{ //nolint:gosec\n\t\t\t\t\t\"GITHUB_TOKEN\": \"${{github.token}}\",\n\t\t\t\t},\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"job has only one step\",\n\t\t\tcfg:    &config.Config{},\n\t\t\tjobCtx: &policy.JobContext{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tEnv: map[string]string{ //nolint:gosec\n\t\t\t\t\t\"GITHUB_TOKEN\": \"${{github.token}}\",\n\t\t\t\t},\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"secret should not be set to job's env\",\n\t\t\tcfg:    &config.Config{},\n\t\t\tjobCtx: &policy.JobContext{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tEnv: map[string]string{ //nolint:gosec\n\t\t\t\t\t\"GITHUB_TOKEN\": \"${{secrets.GITHUB_TOKEN}}\",\n\t\t\t\t},\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"github token should not be set to job's env\",\n\t\t\tcfg:    &config.Config{},\n\t\t\tjobCtx: &policy.JobContext{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tEnv: map[string]string{ //nolint:gosec\n\t\t\t\t\t\"GITHUB_TOKEN\": \"${{github.token}}\",\n\t\t\t\t},\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname:   \"pass\",\n\t\t\tcfg:    &config.Config{},\n\t\t\tjobCtx: &policy.JobContext{},\n\t\t\tjob: &workflow.Job{\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"FOO\": \"foo\",\n\t\t\t\t},\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{},\n\t\t\t\t\t{},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tp := policy.NewJobSecretsPolicy()\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyJob(logger, d.cfg, d.jobCtx, d.job); err != nil {\n\t\t\t\tif !d.isErr {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/job_timeout_minutes_is_required.go",
    "content": "package policy\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype JobTimeoutMinutesIsRequiredPolicy struct{}\n\nfunc (p *JobTimeoutMinutesIsRequiredPolicy) Name() string {\n\treturn \"job_timeout_minutes_is_required\"\n}\n\nfunc (p *JobTimeoutMinutesIsRequiredPolicy) ID() string {\n\treturn \"012\"\n}\n\nfunc (p *JobTimeoutMinutesIsRequiredPolicy) ApplyJob(_ *slog.Logger, _ *config.Config, _ *JobContext, job *workflow.Job) error {\n\tif job.TimeoutMinutes != nil {\n\t\treturn nil\n\t}\n\tif job.Uses != \"\" {\n\t\t// when a reusable workflow is called with \"uses\", \"timeout-minutes\" is not available.\n\t\treturn nil\n\t}\n\tfor _, step := range job.Steps {\n\t\tif step.TimeoutMinutes == nil {\n\t\t\treturn errors.New(\"job's timeout-minutes is required\")\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/policy/job_timeout_minutes_is_required_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestJobTimeoutMinutesIsRequiredPolicy_ApplyJob(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname  string\n\t\tjob   *workflow.Job\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"normal\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tTimeoutMinutes: 30,\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{\n\t\t\t\t\t\tRun: \"echo hello\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"expression is used\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tTimeoutMinutes: \"${{ matrix.timeout-minutes }}\",\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{\n\t\t\t\t\t\tRun: \"echo hello\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"workflow using reusable workflow\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tUses: \"suzuki-shunsuke/renovate-config-validator-workflow/.github/workflows/validate.yaml@v0.2.3\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"job should have timeout-minutes\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{\n\t\t\t\t\t\tRun: \"echo hello\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"all steps have timeout-minutes\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{\n\t\t\t\t\t\tRun:            \"echo hello\",\n\t\t\t\t\t\tTimeoutMinutes: 60,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tRun:            \"echo hello\",\n\t\t\t\t\t\tTimeoutMinutes: 60,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"expression is used in step's timeout-minutes\",\n\t\t\tjob: &workflow.Job{\n\t\t\t\tSteps: []*workflow.Step{\n\t\t\t\t\t{\n\t\t\t\t\t\tRun:            \"echo hello\",\n\t\t\t\t\t\tTimeoutMinutes: \"${{ matrix.timeout-minutes }}\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tRun:            \"echo hello\",\n\t\t\t\t\t\tTimeoutMinutes: 60,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tp := &policy.JobTimeoutMinutesIsRequiredPolicy{}\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyJob(logger, nil, nil, d.job); err != nil {\n\t\t\t\tif !d.isErr {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/policy/workflow_secrets_policy.go",
    "content": "package policy\n\nimport (\n\t\"log/slog\"\n\t\"regexp\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\ntype WorkflowSecretsPolicy struct {\n\tsecretPattern      *regexp.Regexp\n\tgithubTokenPattern *regexp.Regexp\n}\n\nfunc NewWorkflowSecretsPolicy() *WorkflowSecretsPolicy {\n\treturn &WorkflowSecretsPolicy{\n\t\tsecretPattern:      regexp.MustCompile(`\\${{ *secrets\\.[^ ]+ *}}`),\n\t\tgithubTokenPattern: regexp.MustCompile(`\\${{ *github\\.token+ *}}`),\n\t}\n}\n\nfunc (p *WorkflowSecretsPolicy) Name() string {\n\treturn \"workflow_secrets\"\n}\n\nfunc (p *WorkflowSecretsPolicy) ID() string {\n\treturn \"005\"\n}\n\nfunc (p *WorkflowSecretsPolicy) ApplyWorkflow(logger *slog.Logger, _ *config.Config, _ *WorkflowContext, wf *workflow.Workflow) error {\n\tif len(wf.Jobs) < 2 { //nolint:mnd\n\t\treturn nil\n\t}\n\tfailed := false\n\tfor envName, envValue := range wf.Env {\n\t\tif p.secretPattern.MatchString(envValue) {\n\t\t\tfailed = true\n\t\t\tlogger.Error(\"secret should not be set to workflow's env\", \"env_name\", envName)\n\t\t}\n\t\tif p.githubTokenPattern.MatchString(envValue) {\n\t\t\tfailed = true\n\t\t\tlogger.Error(\"github.token should not be set to workflow's env\", \"env_name\", envName)\n\t\t}\n\t}\n\tif failed {\n\t\treturn errEmpty\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/policy/workflow_secrets_policy_test.go",
    "content": "package policy_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/config\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/policy\"\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n)\n\nfunc TestWorkflowSecretsPolicy_ApplyWorkflow(t *testing.T) { //nolint:funlen\n\tt.Parallel()\n\tdata := []struct {\n\t\tname  string\n\t\tcfg   *config.Config\n\t\twf    *workflow.Workflow\n\t\tisErr bool\n\t}{\n\t\t{\n\t\t\tname: \"workflow has only one job\",\n\t\t\tcfg:  &config.Config{},\n\t\t\twf: &workflow.Workflow{\n\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\tEnv: map[string]string{ //nolint:gosec\n\t\t\t\t\t\"GITHUB_TOKEN\": \"${{github.token}}\",\n\t\t\t\t},\n\t\t\t\tJobs: map[string]*workflow.Job{\n\t\t\t\t\t\"foo\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"secret should not be set to workflow's env\",\n\t\t\tcfg:  &config.Config{},\n\t\t\twf: &workflow.Workflow{\n\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\tEnv: map[string]string{ //nolint:gosec\n\t\t\t\t\t\"GITHUB_TOKEN\": \"${{secrets.GITHUB_TOKEN}}\",\n\t\t\t\t},\n\t\t\t\tJobs: map[string]*workflow.Job{\n\t\t\t\t\t\"foo\": {},\n\t\t\t\t\t\"bar\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"github token should not be set to workflow's env\",\n\t\t\tcfg:  &config.Config{},\n\t\t\twf: &workflow.Workflow{\n\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\tEnv: map[string]string{ //nolint:gosec\n\t\t\t\t\t\"GITHUB_TOKEN\": \"${{github.token}}\",\n\t\t\t\t},\n\t\t\t\tJobs: map[string]*workflow.Job{\n\t\t\t\t\t\"foo\": {},\n\t\t\t\t\t\"bar\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t\tisErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"pass\",\n\t\t\tcfg:  &config.Config{},\n\t\t\twf: &workflow.Workflow{\n\t\t\t\tFilePath: \".github/workflows/test.yaml\",\n\t\t\t\tEnv: map[string]string{\n\t\t\t\t\t\"FOO\": \"foo\",\n\t\t\t\t},\n\t\t\t\tJobs: map[string]*workflow.Job{\n\t\t\t\t\t\"foo\": {},\n\t\t\t\t\t\"bar\": {},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tp := policy.NewWorkflowSecretsPolicy()\n\tlogger := slog.New(slog.DiscardHandler)\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tif err := p.ApplyWorkflow(logger, d.cfg, nil, d.wf); err != nil {\n\t\t\t\tif !d.isErr {\n\t\t\t\t\tt.Fatal(err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif d.isErr {\n\t\t\t\tt.Fatal(\"error must be returned\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/workflow/container.go",
    "content": "package workflow\n\nimport (\n\t\"errors\"\n)\n\ntype Container struct {\n\tImage string\n}\n\nfunc (c *Container) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar val any\n\tif err := unmarshal(&val); err != nil {\n\t\treturn err\n\t}\n\treturn convContainer(val, c)\n}\n\nfunc convContainer(src any, c *Container) error { //nolint:cyclop\n\tswitch p := src.(type) {\n\tcase string:\n\t\tc.Image = p\n\t\treturn nil\n\tcase map[any]any:\n\t\tfor k, v := range p {\n\t\t\tkey, ok := k.(string)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif key != \"image\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\timage, ok := v.(string)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"image must be a string\")\n\t\t\t}\n\t\t\tc.Image = image\n\t\t\treturn nil\n\t\t}\n\t\treturn nil\n\tcase map[string]any:\n\t\tfor k, v := range p {\n\t\t\tif k != \"image\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\timage, ok := v.(string)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"image must be a string\")\n\t\t\t}\n\t\t\tc.Image = image\n\t\t\treturn nil\n\t\t}\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"container must be a map or string\")\n\t}\n}\n"
  },
  {
    "path": "pkg/workflow/container_test.go",
    "content": "package workflow_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestContainer_UnmarshalYAML(t *testing.T) {\n\tt.Parallel()\n\tdata := []struct {\n\t\tname  string\n\t\tyaml  string\n\t\timage string\n\t}{\n\t\t{\n\t\t\tname:  \"normal\",\n\t\t\tyaml:  \"image: node:18\",\n\t\t\timage: \"node:18\",\n\t\t},\n\t\t{\n\t\t\tname:  \"string\",\n\t\t\tyaml:  \"node:18\",\n\t\t\timage: \"node:18\",\n\t\t},\n\t}\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tc := &workflow.Container{}\n\t\t\tif err := yaml.Unmarshal([]byte(d.yaml), c); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif d.image != c.Image {\n\t\t\t\tt.Fatalf(\"got %v, wanted %v\", c.Image, d.image)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/workflow/job_secrets.go",
    "content": "package workflow\n\nimport (\n\t\"errors\"\n\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n)\n\ntype JobSecrets struct {\n\tm       map[string]string\n\tinherit bool\n}\n\nfunc (js *JobSecrets) Secrets() map[string]string {\n\treturn js.m\n}\n\nfunc (js *JobSecrets) Inherit() bool {\n\treturn js != nil && js.inherit\n}\n\nfunc (js *JobSecrets) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar val any\n\tif err := unmarshal(&val); err != nil {\n\t\treturn err\n\t}\n\treturn convJobSecrets(val, js)\n}\n\nfunc convJobSecrets(src any, dest *JobSecrets) error { //nolint:cyclop\n\tswitch p := src.(type) {\n\tcase string:\n\t\tswitch p {\n\t\tcase \"inherit\":\n\t\t\tdest.inherit = true\n\t\t\treturn nil\n\t\tdefault:\n\t\t\treturn slogerr.With(errors.New(\"job secrets must be a map or `inherit`\"), \"secrets\", p) //nolint:wrapcheck\n\t\t}\n\tcase map[any]any:\n\t\tm := make(map[string]string, len(p))\n\t\tfor k, v := range p {\n\t\t\tks, ok := k.(string)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"secrets key must be string\")\n\t\t\t}\n\t\t\tvs, ok := v.(string)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"secrets value must be string\")\n\t\t\t}\n\t\t\tm[ks] = vs\n\t\t}\n\t\tdest.m = m\n\t\treturn nil\n\tcase map[string]any:\n\t\tm := make(map[string]string, len(p))\n\t\tfor k, v := range p {\n\t\t\tvs, ok := v.(string)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"secrets value must be string\")\n\t\t\t}\n\t\t\tm[k] = vs\n\t\t}\n\t\tdest.m = m\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"secrets must be map[string]string or 'inherit'\")\n\t}\n}\n"
  },
  {
    "path": "pkg/workflow/job_secrets_test.go",
    "content": "package workflow_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestJobSecrets_UnmarshalYAML(t *testing.T) {\n\tt.Parallel()\n\tdata := []struct {\n\t\tname    string\n\t\tyaml    string\n\t\tinherit bool\n\t}{\n\t\t{\n\t\t\tname: \"not inherit\",\n\t\t\tyaml: `token: ${{github.token}}`,\n\t\t},\n\t\t{\n\t\t\tname:    \"inherit\",\n\t\t\tyaml:    `inherit`,\n\t\t\tinherit: true,\n\t\t},\n\t}\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tjs := &workflow.JobSecrets{}\n\t\t\tif err := yaml.Unmarshal([]byte(d.yaml), js); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tinherit := js.Inherit()\n\t\t\tif d.inherit != inherit {\n\t\t\t\tt.Fatalf(\"got %v, wanted %v\", inherit, d.inherit)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/workflow/list_workflows.go",
    "content": "package workflow\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/afero\"\n)\n\nfunc List(fs afero.Fs) ([]string, error) {\n\tfiles, err := afero.Glob(fs, \".github/workflows/*.yml\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"find .github/workflows/*.yml: %w\", err)\n\t}\n\tfiles2, err := afero.Glob(fs, \".github/workflows/*.yaml\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"find .github/workflows/*.yaml: %w\", err)\n\t}\n\treturn append(files, files2...), nil\n}\n"
  },
  {
    "path": "pkg/workflow/permissions.go",
    "content": "package workflow\n\nimport (\n\t\"errors\"\n\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n)\n\ntype Permissions struct {\n\tm        map[string]string\n\treadAll  bool\n\twriteAll bool\n}\n\nfunc NewPermissions(readAll, writeAll bool, m map[string]string) *Permissions {\n\treturn &Permissions{\n\t\tm:        m,\n\t\treadAll:  readAll,\n\t\twriteAll: writeAll,\n\t}\n}\n\nfunc (ps *Permissions) Permissions() map[string]string {\n\tif ps == nil {\n\t\treturn nil\n\t}\n\treturn ps.m\n}\n\nfunc (ps *Permissions) ReadAll() bool {\n\tif ps == nil {\n\t\treturn false\n\t}\n\treturn ps.readAll\n}\n\nfunc (ps *Permissions) WriteAll() bool {\n\tif ps == nil {\n\t\treturn false\n\t}\n\treturn ps.writeAll\n}\n\nfunc (ps *Permissions) IsNil() bool {\n\tif ps == nil {\n\t\treturn true\n\t}\n\treturn ps.m == nil && !ps.readAll && !ps.writeAll\n}\n\nfunc (ps *Permissions) UnmarshalYAML(unmarshal func(any) error) error {\n\tvar val any\n\tif err := unmarshal(&val); err != nil {\n\t\treturn err\n\t}\n\treturn convPermissions(val, ps)\n}\n\nfunc convPermissions(src any, dest *Permissions) error { //nolint:cyclop\n\tswitch p := src.(type) {\n\tcase string:\n\t\tswitch p {\n\t\tcase \"read-all\":\n\t\t\tdest.readAll = true\n\t\t\treturn nil\n\t\tcase \"write-all\":\n\t\t\tdest.writeAll = true\n\t\t\treturn nil\n\t\tdefault:\n\t\t\treturn slogerr.With(errors.New(\"unknown permissions\"), \"permission\", p) //nolint:wrapcheck\n\t\t}\n\tcase map[any]any:\n\t\tm := make(map[string]string, len(p))\n\t\tfor k, v := range p {\n\t\t\tks, ok := k.(string)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"permissions key must be string\")\n\t\t\t}\n\t\t\tvs, ok := v.(string)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"permissions value must be string\")\n\t\t\t}\n\t\t\tm[ks] = vs\n\t\t}\n\t\tdest.m = m\n\t\treturn nil\n\tcase map[string]any:\n\t\tm := make(map[string]string, len(p))\n\t\tfor k, v := range p {\n\t\t\tvs, ok := v.(string)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"permissions value must be string\")\n\t\t\t}\n\t\t\tm[k] = vs\n\t\t}\n\t\tdest.m = m\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"permissions must be map[string]string or 'read-all' or 'write-all'\")\n\t}\n}\n"
  },
  {
    "path": "pkg/workflow/permissions_test.go",
    "content": "package workflow_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/suzuki-shunsuke/ghalint/pkg/workflow\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestPermissions_UnmarshalYAML(t *testing.T) {\n\tt.Parallel()\n\tdata := []struct {\n\t\tname     string\n\t\tyaml     string\n\t\treadAll  bool\n\t\twriteAll bool\n\t}{\n\t\t{\n\t\t\tname: \"not read-all and write-all\",\n\t\t\tyaml: `contents: read`,\n\t\t},\n\t\t{\n\t\t\tname:    \"read-all\",\n\t\t\tyaml:    `read-all`,\n\t\t\treadAll: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"write-all\",\n\t\t\tyaml:     `write-all`,\n\t\t\twriteAll: true,\n\t\t},\n\t}\n\tfor _, d := range data {\n\t\tt.Run(d.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tp := &workflow.Permissions{}\n\t\t\tif err := yaml.Unmarshal([]byte(d.yaml), p); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\treadAll := p.ReadAll()\n\t\t\twriteAll := p.WriteAll()\n\t\t\tif d.readAll != readAll {\n\t\t\t\tt.Fatalf(\"readAll got %v, wanted %v\", readAll, d.readAll)\n\t\t\t}\n\t\t\tif d.writeAll != writeAll {\n\t\t\t\tt.Fatalf(\"writeAll got %v, wanted %v\", writeAll, d.writeAll)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/workflow/read_action.go",
    "content": "package workflow\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc ReadAction(fs afero.Fs, p string, action *Action) error {\n\tf, err := fs.Open(p)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"open an action file: %w\", err)\n\t}\n\tdefer f.Close()\n\tif err := yaml.NewDecoder(f).Decode(action); err != nil {\n\t\terr := fmt.Errorf(\"parse an action file as YAML: %w\", err)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\treturn slogerr.With(err, \"reference\", \"https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/codes/001.md\") //nolint:wrapcheck\n\t\t}\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/workflow/read_workflow.go",
    "content": "package workflow\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/spf13/afero\"\n\t\"github.com/suzuki-shunsuke/slog-error/slogerr\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc Read(fs afero.Fs, p string, wf *Workflow) error {\n\tf, err := fs.Open(p)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"open a workflow file: %w\", err)\n\t}\n\tdefer f.Close()\n\tif err := yaml.NewDecoder(f).Decode(wf); err != nil {\n\t\terr := fmt.Errorf(\"parse a workflow file as YAML: %w\", err)\n\t\tif errors.Is(err, io.EOF) {\n\t\t\treturn slogerr.With(err, \"reference\", \"https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/codes/001.md\") //nolint:wrapcheck\n\t\t}\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/workflow/workflow.go",
    "content": "package workflow\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype Workflow struct {\n\tFilePath    string `yaml:\"-\"`\n\tJobs        map[string]*Job\n\tEnv         map[string]string\n\tPermissions *Permissions\n}\n\ntype Job struct {\n\tPermissions    *Permissions\n\tEnv            map[string]string\n\tSteps          []*Step\n\tSecrets        *JobSecrets\n\tContainer      *Container\n\tUses           string\n\tTimeoutMinutes any `yaml:\"timeout-minutes\"`\n\tWith           map[string]any\n}\n\ntype Step struct {\n\tUses           string\n\tID             string\n\tName           string\n\tRun            string\n\tShell          string\n\tWith           With\n\tTimeoutMinutes any `yaml:\"timeout-minutes\"`\n}\n\ntype With map[string]string\n\nfunc (w With) UnmarshalYAML(b []byte) error {\n\ta := map[string]any{}\n\tif err := yaml.Unmarshal(b, &a); err != nil {\n\t\treturn err //nolint:wrapcheck\n\t}\n\tfor k, v := range a {\n\t\tswitch c := v.(type) {\n\t\tcase string:\n\t\t\tw[k] = c\n\t\tcase int:\n\t\t\tw[k] = strconv.Itoa(c)\n\t\tcase float64:\n\t\t\tw[k] = fmt.Sprint(c)\n\t\tcase bool:\n\t\t\tw[k] = strconv.FormatBool(c)\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unsupported type: %T\", c)\n\t\t}\n\t}\n\treturn nil\n}\n\ntype Action struct {\n\tRuns   *Runs\n\tInputs map[string]*Input\n}\n\ntype Runs struct {\n\tImage string\n\tSteps []*Step\n}\n\ntype Input struct {\n\tRequired bool\n\tType     string\n}\n"
  },
  {
    "path": "renovate.json5",
    "content": "{\n  extends: [\n    \"github>suzuki-shunsuke/renovate-config#4.0.0\",\n    \"github>suzuki-shunsuke/renovate-config:nolimit#4.0.0\",\n    \"github>suzuki-shunsuke/renovate-config:go-directive#4.0.0\",\n    \"github>aquaproj/aqua-renovate-config#2.12.1\",\n    \"github>aquaproj/aqua-renovate-config:file#2.12.1(aqua/imports/.*\\\\.ya?ml)\",\n  ],\n}\n"
  },
  {
    "path": "scripts/coverage.sh",
    "content": "#!/usr/bin/env bash\n\nset -eu\nset -o pipefail\n\ncd \"$(dirname \"$0\")/..\"\n\nif [ $# -eq 0 ]; then\n  target=\"$(go list ./... | fzf)\"\n  profile=.coverage/$target/coverage.txt\n  mkdir -p .coverage/\"$target\"\nelif [ $# -eq 1 ]; then\n  target=$1\n  mkdir -p .coverage/\"$target\"\n  profile=.coverage/$target/coverage.txt\n  target=./$target\nelse\n  echo \"too many arguments are given: $*\" >&2\n  exit 1\nfi\n\ngo test \"$target\" -coverprofile=\"$profile\" -covermode=atomic\ngo tool cover -html=\"$profile\"\n"
  },
  {
    "path": "scripts/generate-usage.sh",
    "content": "#!/usr/bin/env bash\n\nset -eu\n\ncd \"$(dirname \"$0\")/..\"\n\nhelp=$(ghalint help-all)\n\necho -n \"# Usage\n\n<!-- This is generated by scripts/generate-usage.sh. Don't edit this file directly. -->\n\n$help\" > docs/usage.md\n"
  },
  {
    "path": "test-action.yaml",
    "content": "name: test\ndescription: test\ninputs:\n  github_token:\n    description: \"\"\n    required: false\n    default: ${{ github.token }}\nruns:\n  using: composite\n  steps:\n    # checkout_persist_credentials_should_be_false\n    - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n\n    # action_ref_should_be_full_length_commit_sha\n    - uses: tibdex/github-app-token@v2.1.0\n      id: token1\n      with:\n        app_id: ${{secrets.APP_ID}}\n        private_key: ${{secrets.PRIVATE_KEY}}\n        # github_app_should_limit_repositories\n        # github_app_should_limit_permissions\n\n    - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0\n      id: token2\n      with:\n        app_id: ${{secrets.APP_ID}}\n        private_key: ${{secrets.PRIVATE_KEY}}\n        repositories: >-\n          [\"${{github.event.repository.name}}\"]\n        permissions: >-\n          {\n            \"contents\": \"write\"\n          }\n\n    - uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1\n      id: token3\n      with:\n        app-id: ${{vars.APP_ID}}\n        private-key: ${{secrets.PRIVATE_KEY}}\n        owner: ${{github.repository_owner}}\n        # github_app_should_limit_repositories\n\n    - uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1\n      id: token4\n      with:\n        app-id: ${{vars.APP_ID}}\n        private-key: ${{secrets.PRIVATE_KEY}}\n        owner: ${{github.repository_owner}}\n        repositories: \"repo1,repo2\"\n\n    - uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1\n      id: token5\n      with:\n        app-id: ${{vars.APP_ID}}\n        private-key: ${{secrets.PRIVATE_KEY}}\n\n    - run: echo hello\n      # action_shell_is_required\n"
  },
  {
    "path": "test-workflow.yaml",
    "content": "name: test\non: pull_request\nenv:\n  # Workflow should not set secrets to environment variables\n  FOO: bar\n  GITHUB_TOKEN: ${{github.token}}\n  API_KEY: ${{secrets.API_KEY}}\njobs:\n  release:\n    # action_ref_should_be_full_length_commit_sha\n    uses: suzuki-shunsuke/go-release-workflow/.github/workflows/release.yaml@v0.5.0\n    # deny_inherit_secrets\n    secrets: inherit\n    permissions: {}\n\n  foo:\n    # job_permissions\n    runs-on: ubuntu-latest\n    env:\n      # job_secrets\n      FOO: bar\n      GITHUB_TOKEN: ${{github.token}}\n      API_KEY: ${{secrets.API_KEY}}\n    steps:\n      # checkout_persist_credentials_should_be_false\n      - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7\n      - run: echo hello\n      - run: echo hello\n\n  read-all:\n    runs-on: ubuntu-latest\n    # deny_read_all_permission\n    permissions: read-all\n    env:\n      # If the job has only one job, it's okay to set secrets to job's environment variables\n      FOO: bar\n      GITHUB_TOKEN: ${{github.token}}\n      API_KEY: ${{secrets.API_KEY}}\n    steps:\n      - run: echo hello\n\n  write-all:\n    runs-on: ubuntu-latest\n    # deny_write_all_permission\n    permissions: write-all\n    steps:\n      # action_ref_should_be_full_length_commit_sha\n      - uses: tibdex/github-app-token@v2.1.0\n        id: token1\n        with:\n          app_id: ${{secrets.APP_ID}}\n          private_key: ${{secrets.PRIVATE_KEY}}\n          # github_app_should_limit_repositories\n          # github_app_should_limit_permissions\n\n      - uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a # v2.1.0\n        id: token2\n        with:\n          app_id: ${{secrets.APP_ID}}\n          private_key: ${{secrets.PRIVATE_KEY}}\n          repositories: >-\n            [\"${{github.event.repository.name}}\"]\n          permissions: >-\n            {\n              \"contents\": \"write\"\n            }\n\n      - uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1\n        id: token3\n        with:\n          app-id: ${{vars.APP_ID}}\n          private-key: ${{secrets.PRIVATE_KEY}}\n          owner: ${{github.repository_owner}}\n          # github_app_should_limit_repositories\n\n      - uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1\n        id: token4\n        with:\n          app-id: ${{vars.APP_ID}}\n          private-key: ${{secrets.PRIVATE_KEY}}\n          owner: ${{github.repository_owner}}\n          repositories: \"repo1,repo2\"\n\n      - uses: actions/create-github-app-token@46e4a501e119d39574a54e53a06c9a705efc55c9 # v1.6.1\n        id: token5\n        with:\n          app-id: ${{vars.APP_ID}}\n          private-key: ${{secrets.PRIVATE_KEY}}\n\n  container-job:\n    runs-on: ubuntu-latest\n    permissions: {}\n    container:\n      image: node:latest # deny_job_container_latest_image\n"
  }
]