actionscope 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- actionscope-0.1.0/.gitignore +12 -0
- actionscope-0.1.0/CHANGELOG.md +23 -0
- actionscope-0.1.0/CONTRIBUTING.md +34 -0
- actionscope-0.1.0/LICENSE +21 -0
- actionscope-0.1.0/PKG-INFO +132 -0
- actionscope-0.1.0/README.md +89 -0
- actionscope-0.1.0/action.yml +109 -0
- actionscope-0.1.0/actionscope/__init__.py +6 -0
- actionscope-0.1.0/actionscope/analyzers/__init__.py +3 -0
- actionscope-0.1.0/actionscope/analyzers/github_token.py +201 -0
- actionscope-0.1.0/actionscope/analyzers/iam_risk.py +367 -0
- actionscope-0.1.0/actionscope/analyzers/privesc_detector.py +240 -0
- actionscope-0.1.0/actionscope/analyzers/risk_engine.py +217 -0
- actionscope-0.1.0/actionscope/cli.py +245 -0
- actionscope-0.1.0/actionscope/models.py +193 -0
- actionscope-0.1.0/actionscope/parsers/__init__.py +3 -0
- actionscope-0.1.0/actionscope/parsers/policy_json.py +260 -0
- actionscope-0.1.0/actionscope/parsers/terraform.py +386 -0
- actionscope-0.1.0/actionscope/parsers/workflow.py +187 -0
- actionscope-0.1.0/actionscope/reporters/__init__.py +3 -0
- actionscope-0.1.0/actionscope/reporters/json_reporter.py +142 -0
- actionscope-0.1.0/actionscope/reporters/markdown.py +244 -0
- actionscope-0.1.0/actionscope/reporters/terminal.py +394 -0
- actionscope-0.1.0/actionscope/verifiers/__init__.py +3 -0
- actionscope-0.1.0/actionscope/verifiers/aws_verifier.py +363 -0
- actionscope-0.1.0/docs/aws-verify-permissions.md +30 -0
- actionscope-0.1.0/pyproject.toml +96 -0
- actionscope-0.1.0/research/FINDINGS.md +132 -0
- actionscope-0.1.0/research/README.md +46 -0
- actionscope-0.1.0/research/generate_report.py +209 -0
- actionscope-0.1.0/research/launch_posts.md +161 -0
- actionscope-0.1.0/research/methodology.md +63 -0
- actionscope-0.1.0/research/scan_public_repos.py +474 -0
- actionscope-0.1.0/scripts/bump_version.py +35 -0
- actionscope-0.1.0/scripts/pre_release_check.py +65 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to ActionScope are documented here.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-05-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- GitHub Actions workflow parser: detects AWS credential configurations
|
|
9
|
+
- IAM policy risk classifier using policy-sentry action database
|
|
10
|
+
- Terraform HCL parser for IAM resource definitions
|
|
11
|
+
- JSON IAM policy file parser
|
|
12
|
+
- GITHUB_TOKEN permission analyzer
|
|
13
|
+
- Privilege escalation path detector (8 common paths)
|
|
14
|
+
- Terminal reporter with Rich color output
|
|
15
|
+
- JSON output format for CI integration
|
|
16
|
+
- Markdown output format for GitHub PR comments
|
|
17
|
+
- GitHub Action integration (actionscope@v0)
|
|
18
|
+
- `--aws-verify` flag for live AWS IAM API verification
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
- All analysis is read-only and deterministic
|
|
22
|
+
- No data is sent to external services
|
|
23
|
+
- AWS verification uses minimum required IAM permissions
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Contributing to ActionScope
|
|
2
|
+
|
|
3
|
+
## Adding a New IAM Risk Rule
|
|
4
|
+
|
|
5
|
+
The easiest contribution is adding IAM action risk classifications.
|
|
6
|
+
See `actionscope/analyzers/iam_risk.py` — the `ALWAYS_CRITICAL` set.
|
|
7
|
+
|
|
8
|
+
To add a new always-critical action:
|
|
9
|
+
|
|
10
|
+
1. Add it to the `ALWAYS_CRITICAL` set in iam_risk.py
|
|
11
|
+
2. Add a test in tests/test_iam_risk.py
|
|
12
|
+
3. Open a PR
|
|
13
|
+
|
|
14
|
+
## Adding a New Workflow Parser Pattern
|
|
15
|
+
|
|
16
|
+
ActionScope currently detects `aws-actions/configure-aws-credentials`.
|
|
17
|
+
To add support for another credential provider (e.g. Google Cloud):
|
|
18
|
+
|
|
19
|
+
1. Add a parser function in `actionscope/parsers/workflow.py`
|
|
20
|
+
2. Add a new CredentialSource type in models.py if needed
|
|
21
|
+
3. Add test fixtures in tests/fixtures/workflows/
|
|
22
|
+
4. Open a PR
|
|
23
|
+
|
|
24
|
+
## Running Tests
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install -e ".[dev]"
|
|
28
|
+
pytest tests/ -v
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Self-Scan
|
|
32
|
+
|
|
33
|
+
ActionScope scans itself in CI. This repo intentionally uses minimal
|
|
34
|
+
permissions (contents: read only) to demonstrate good practice.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rishabh Singh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: actionscope
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Map the AWS blast radius of GitHub Actions workflows
|
|
5
|
+
Project-URL: Homepage, https://github.com/r12habh/ActionScope
|
|
6
|
+
Project-URL: Documentation, https://github.com/r12habh/ActionScope#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/r12habh/ActionScope
|
|
8
|
+
Project-URL: Issues, https://github.com/r12habh/ActionScope/issues
|
|
9
|
+
Author-email: Rishabh Singh <rishabhsinghe@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: aws,blast-radius,cicd,devops,devsecops,github-actions,iam,security
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Security
|
|
22
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: click>=8.0
|
|
25
|
+
Requires-Dist: policy-sentry>=0.12.0
|
|
26
|
+
Requires-Dist: python-hcl2>=4.0
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: rich>=13.0
|
|
29
|
+
Provides-Extra: aws
|
|
30
|
+
Requires-Dist: boto3>=1.26; extra == 'aws'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: hatchling>=1.25; extra == 'dev'
|
|
34
|
+
Requires-Dist: moto[iam]>=5.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
39
|
+
Provides-Extra: research
|
|
40
|
+
Requires-Dist: requests>=2.31; extra == 'research'
|
|
41
|
+
Requires-Dist: tqdm>=4.66; extra == 'research'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# ActionScope
|
|
45
|
+
|
|
46
|
+
> Map the AWS blast radius of your GitHub Actions workflows.
|
|
47
|
+
|
|
48
|
+
[](https://pypi.org/project/actionscope/)
|
|
49
|
+
[](LICENSE)
|
|
50
|
+
|
|
51
|
+
ActionScope reads your `.github/workflows/` files, Terraform IAM resources,
|
|
52
|
+
and inline JSON IAM policies, then tells you — in plain English — what your
|
|
53
|
+
CI/CD pipelines can actually do to your AWS environment.
|
|
54
|
+
|
|
55
|
+
**It answers the question no other tool answers:**
|
|
56
|
+
"If this workflow is compromised, what can an attacker do in AWS?"
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install actionscope
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
actionscope scan .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Example Output
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
ActionScope — Blast Radius Report
|
|
74
|
+
Path: /my-repo | Workflows: 2 | Overall Risk: 🔴 CRITICAL
|
|
75
|
+
|
|
76
|
+
deploy.yml → deploy → Configure AWS credentials
|
|
77
|
+
AWS Role: arn:aws:iam::123456789012:role/github-deploy-role
|
|
78
|
+
Auth: OIDC ✓
|
|
79
|
+
|
|
80
|
+
┌─────────────────────────────┬────────────────────┬──────────┐
|
|
81
|
+
│ Action │ Access Level │ Risk │
|
|
82
|
+
├─────────────────────────────┼────────────────────┼──────────┤
|
|
83
|
+
│ iam:PassRole │ Permissions mgmt │ 🔴 CRIT │
|
|
84
|
+
│ ec2:TerminateInstances │ Write │ 🟠 HIGH │
|
|
85
|
+
│ s3:GetObject │ Read │ 🟢 LOW │
|
|
86
|
+
└─────────────────────────────┴────────────────────┴──────────┘
|
|
87
|
+
|
|
88
|
+
⚠️ iam:PassRole on * — privilege escalation path exists
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Use as a GitHub Action
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
- uses: r12habh/ActionScope@v0
|
|
95
|
+
with:
|
|
96
|
+
fail-on: high
|
|
97
|
+
comment-pr: true
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## How It Works
|
|
101
|
+
|
|
102
|
+
ActionScope performs **static analysis only** — it never sends your code to
|
|
103
|
+
any external service.
|
|
104
|
+
|
|
105
|
+
1. Finds all `.github/workflows/*.yml` files
|
|
106
|
+
2. Extracts AWS role ARNs and GITHUB_TOKEN permission declarations
|
|
107
|
+
3. Finds matching IAM policies in Terraform or JSON files in your repo
|
|
108
|
+
4. Classifies each IAM action by risk using the
|
|
109
|
+
[policy-sentry](https://github.com/salesforce/policy_sentry) database
|
|
110
|
+
5. Outputs a plain-English blast radius report
|
|
111
|
+
|
|
112
|
+
### What If My Policies Aren't in the Repo?
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
ℹ️ Policy not found in repo for role: arn:aws:iam::123456:role/ci-deploy
|
|
116
|
+
💡 Run with --aws-verify to fetch live policies from AWS (coming in v1.0)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
In v1.0, `--aws-verify` will use read-only AWS API calls to fetch the real
|
|
120
|
+
attached policies for any role ARN found in your workflows.
|
|
121
|
+
|
|
122
|
+
## Public Research
|
|
123
|
+
|
|
124
|
+
ActionScope includes a reproducible public-data research scaffold for analyzing
|
|
125
|
+
workflow-level AWS security patterns across public GitHub repositories. See
|
|
126
|
+
[`research/`](research/) for the scanner, methodology, and anonymized findings
|
|
127
|
+
template.
|
|
128
|
+
|
|
129
|
+
## Built By
|
|
130
|
+
|
|
131
|
+
Rishabh Singh — AWS Security Engineer.
|
|
132
|
+
[GitHub](https://github.com/r12habh)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ActionScope
|
|
2
|
+
|
|
3
|
+
> Map the AWS blast radius of your GitHub Actions workflows.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/actionscope/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
ActionScope reads your `.github/workflows/` files, Terraform IAM resources,
|
|
9
|
+
and inline JSON IAM policies, then tells you — in plain English — what your
|
|
10
|
+
CI/CD pipelines can actually do to your AWS environment.
|
|
11
|
+
|
|
12
|
+
**It answers the question no other tool answers:**
|
|
13
|
+
"If this workflow is compromised, what can an attacker do in AWS?"
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install actionscope
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
actionscope scan .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Example Output
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
ActionScope — Blast Radius Report
|
|
31
|
+
Path: /my-repo | Workflows: 2 | Overall Risk: 🔴 CRITICAL
|
|
32
|
+
|
|
33
|
+
deploy.yml → deploy → Configure AWS credentials
|
|
34
|
+
AWS Role: arn:aws:iam::123456789012:role/github-deploy-role
|
|
35
|
+
Auth: OIDC ✓
|
|
36
|
+
|
|
37
|
+
┌─────────────────────────────┬────────────────────┬──────────┐
|
|
38
|
+
│ Action │ Access Level │ Risk │
|
|
39
|
+
├─────────────────────────────┼────────────────────┼──────────┤
|
|
40
|
+
│ iam:PassRole │ Permissions mgmt │ 🔴 CRIT │
|
|
41
|
+
│ ec2:TerminateInstances │ Write │ 🟠 HIGH │
|
|
42
|
+
│ s3:GetObject │ Read │ 🟢 LOW │
|
|
43
|
+
└─────────────────────────────┴────────────────────┴──────────┘
|
|
44
|
+
|
|
45
|
+
⚠️ iam:PassRole on * — privilege escalation path exists
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Use as a GitHub Action
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
- uses: r12habh/ActionScope@v0
|
|
52
|
+
with:
|
|
53
|
+
fail-on: high
|
|
54
|
+
comment-pr: true
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## How It Works
|
|
58
|
+
|
|
59
|
+
ActionScope performs **static analysis only** — it never sends your code to
|
|
60
|
+
any external service.
|
|
61
|
+
|
|
62
|
+
1. Finds all `.github/workflows/*.yml` files
|
|
63
|
+
2. Extracts AWS role ARNs and GITHUB_TOKEN permission declarations
|
|
64
|
+
3. Finds matching IAM policies in Terraform or JSON files in your repo
|
|
65
|
+
4. Classifies each IAM action by risk using the
|
|
66
|
+
[policy-sentry](https://github.com/salesforce/policy_sentry) database
|
|
67
|
+
5. Outputs a plain-English blast radius report
|
|
68
|
+
|
|
69
|
+
### What If My Policies Aren't in the Repo?
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
ℹ️ Policy not found in repo for role: arn:aws:iam::123456:role/ci-deploy
|
|
73
|
+
💡 Run with --aws-verify to fetch live policies from AWS (coming in v1.0)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
In v1.0, `--aws-verify` will use read-only AWS API calls to fetch the real
|
|
77
|
+
attached policies for any role ARN found in your workflows.
|
|
78
|
+
|
|
79
|
+
## Public Research
|
|
80
|
+
|
|
81
|
+
ActionScope includes a reproducible public-data research scaffold for analyzing
|
|
82
|
+
workflow-level AWS security patterns across public GitHub repositories. See
|
|
83
|
+
[`research/`](research/) for the scanner, methodology, and anonymized findings
|
|
84
|
+
template.
|
|
85
|
+
|
|
86
|
+
## Built By
|
|
87
|
+
|
|
88
|
+
Rishabh Singh — AWS Security Engineer.
|
|
89
|
+
[GitHub](https://github.com/r12habh)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
name: 'ActionScope'
|
|
2
|
+
description: 'Map the AWS blast radius of GitHub Actions workflows and AI agent configs'
|
|
3
|
+
author: 'Rishabh Singh'
|
|
4
|
+
|
|
5
|
+
branding:
|
|
6
|
+
icon: 'shield'
|
|
7
|
+
color: 'orange'
|
|
8
|
+
|
|
9
|
+
inputs:
|
|
10
|
+
path:
|
|
11
|
+
description: 'Path to the repository root to scan'
|
|
12
|
+
required: false
|
|
13
|
+
default: '.'
|
|
14
|
+
fail-on:
|
|
15
|
+
description: 'Fail the action if overall risk is at or above this level (critical, high, medium, low)'
|
|
16
|
+
required: false
|
|
17
|
+
default: 'high'
|
|
18
|
+
output-format:
|
|
19
|
+
description: 'Output format: terminal, json, or markdown'
|
|
20
|
+
required: false
|
|
21
|
+
default: 'terminal'
|
|
22
|
+
comment-pr:
|
|
23
|
+
description: 'Post findings as a PR comment (requires pull-requests: write permission)'
|
|
24
|
+
required: false
|
|
25
|
+
default: 'false'
|
|
26
|
+
github-token:
|
|
27
|
+
description: 'GitHub token for posting PR comments'
|
|
28
|
+
required: false
|
|
29
|
+
default: '${{ github.token }}'
|
|
30
|
+
version:
|
|
31
|
+
description: 'ActionScope version to install (default: latest)'
|
|
32
|
+
required: false
|
|
33
|
+
default: ''
|
|
34
|
+
|
|
35
|
+
outputs:
|
|
36
|
+
overall-risk:
|
|
37
|
+
description: 'The overall risk level found (critical, high, medium, low, info)'
|
|
38
|
+
value: ${{ steps.scan.outputs.overall-risk }}
|
|
39
|
+
findings-json:
|
|
40
|
+
description: 'JSON string of all findings'
|
|
41
|
+
value: ${{ steps.scan.outputs.findings-json }}
|
|
42
|
+
credential-sources-count:
|
|
43
|
+
description: 'Number of AWS credential sources found'
|
|
44
|
+
value: ${{ steps.scan.outputs.credential-sources-count }}
|
|
45
|
+
|
|
46
|
+
runs:
|
|
47
|
+
using: 'composite'
|
|
48
|
+
steps:
|
|
49
|
+
- name: Set up Python
|
|
50
|
+
uses: actions/setup-python@v6
|
|
51
|
+
with:
|
|
52
|
+
python-version: '3.11'
|
|
53
|
+
|
|
54
|
+
- name: Install ActionScope
|
|
55
|
+
shell: bash
|
|
56
|
+
run: |
|
|
57
|
+
if [ -n "${{ inputs.version }}" ]; then
|
|
58
|
+
pip install "actionscope==${{ inputs.version }}"
|
|
59
|
+
else
|
|
60
|
+
pip install actionscope
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
- name: Run ActionScope
|
|
64
|
+
id: scan
|
|
65
|
+
shell: bash
|
|
66
|
+
run: |
|
|
67
|
+
actionscope scan ${{ inputs.path }} \
|
|
68
|
+
--output-format json \
|
|
69
|
+
--output-file /tmp/actionscope-results.json || true
|
|
70
|
+
|
|
71
|
+
if [ -f /tmp/actionscope-results.json ]; then
|
|
72
|
+
RISK=$(python3 -c "
|
|
73
|
+
import json
|
|
74
|
+
d = json.load(open('/tmp/actionscope-results.json'))
|
|
75
|
+
print(d.get('overall_risk', 'info'))
|
|
76
|
+
")
|
|
77
|
+
COUNT=$(python3 -c "
|
|
78
|
+
import json
|
|
79
|
+
d = json.load(open('/tmp/actionscope-results.json'))
|
|
80
|
+
print(d.get('summary', {}).get('credential_sources', 0))
|
|
81
|
+
")
|
|
82
|
+
echo "overall-risk=$RISK" >> "$GITHUB_OUTPUT"
|
|
83
|
+
{
|
|
84
|
+
echo 'findings-json<<JSON_EOF'
|
|
85
|
+
cat /tmp/actionscope-results.json
|
|
86
|
+
echo JSON_EOF
|
|
87
|
+
} >> "$GITHUB_OUTPUT"
|
|
88
|
+
echo "credential-sources-count=$COUNT" >> "$GITHUB_OUTPUT"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
- name: Post PR comment
|
|
93
|
+
if: inputs.comment-pr == 'true' && github.event_name == 'pull_request'
|
|
94
|
+
shell: bash
|
|
95
|
+
env:
|
|
96
|
+
GH_TOKEN: ${{ inputs.github-token }}
|
|
97
|
+
run: |
|
|
98
|
+
MARKDOWN=$(actionscope scan ${{ inputs.path }} --output-format markdown 2>/dev/null || echo "ActionScope scan failed")
|
|
99
|
+
gh pr comment ${{ github.event.pull_request.number }} \
|
|
100
|
+
--body "$MARKDOWN" \
|
|
101
|
+
--edit-last 2>/dev/null || \
|
|
102
|
+
gh pr comment ${{ github.event.pull_request.number }} \
|
|
103
|
+
--body "$MARKDOWN"
|
|
104
|
+
|
|
105
|
+
- name: Fail on risk level
|
|
106
|
+
if: inputs.fail-on != ''
|
|
107
|
+
shell: bash
|
|
108
|
+
run: |
|
|
109
|
+
actionscope scan ${{ inputs.path }} --fail-on ${{ inputs.fail-on }}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""GITHUB_TOKEN permission analyzer for workflow-level and job-level scopes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from actionscope.models import GitHubTokenPermission, RiskLevel
|
|
8
|
+
|
|
9
|
+
KNOWN_PERMISSION_SCOPES = (
|
|
10
|
+
"actions",
|
|
11
|
+
"checks",
|
|
12
|
+
"contents",
|
|
13
|
+
"deployments",
|
|
14
|
+
"discussions",
|
|
15
|
+
"id-token",
|
|
16
|
+
"issues",
|
|
17
|
+
"packages",
|
|
18
|
+
"pages",
|
|
19
|
+
"pull-requests",
|
|
20
|
+
"repository-projects",
|
|
21
|
+
"security-events",
|
|
22
|
+
"statuses",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
HIGH_WRITE_SCOPES = {"pull-requests", "packages", "id-token"}
|
|
26
|
+
MEDIUM_WRITE_SCOPES = {"contents", "actions", "deployments"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def analyze_workflow_permissions(
|
|
30
|
+
workflow_data: dict,
|
|
31
|
+
workflow_file: str,
|
|
32
|
+
) -> list[GitHubTokenPermission]:
|
|
33
|
+
"""Return GITHUB_TOKEN permission findings from parsed workflow YAML."""
|
|
34
|
+
if not isinstance(workflow_data, dict):
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
findings: list[GitHubTokenPermission] = []
|
|
38
|
+
|
|
39
|
+
if "permissions" in workflow_data and workflow_data["permissions"] is not None:
|
|
40
|
+
findings.extend(
|
|
41
|
+
_permissions_to_findings(
|
|
42
|
+
permissions=workflow_data["permissions"],
|
|
43
|
+
workflow_file=workflow_file,
|
|
44
|
+
job_name="",
|
|
45
|
+
oidc_expected=_workflow_uses_role_to_assume(workflow_data),
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
jobs = workflow_data.get("jobs")
|
|
50
|
+
if jobs is None:
|
|
51
|
+
jobs = {}
|
|
52
|
+
if isinstance(jobs, dict):
|
|
53
|
+
for job_name, job_data in jobs.items():
|
|
54
|
+
if not isinstance(job_data, dict):
|
|
55
|
+
continue
|
|
56
|
+
if "permissions" not in job_data or job_data["permissions"] is None:
|
|
57
|
+
continue
|
|
58
|
+
findings.extend(
|
|
59
|
+
_permissions_to_findings(
|
|
60
|
+
permissions=job_data["permissions"],
|
|
61
|
+
workflow_file=workflow_file,
|
|
62
|
+
job_name=str(job_name),
|
|
63
|
+
oidc_expected=_job_uses_role_to_assume(job_data),
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return findings
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_dangerous_token_permissions(
|
|
71
|
+
perms: list[GitHubTokenPermission],
|
|
72
|
+
) -> list[GitHubTokenPermission]:
|
|
73
|
+
"""Return only permissions with risk MEDIUM or higher."""
|
|
74
|
+
return [
|
|
75
|
+
permission
|
|
76
|
+
for permission in perms
|
|
77
|
+
if permission.risk_level >= RiskLevel.MEDIUM
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def summarize_token_risk(perms: list[GitHubTokenPermission]) -> dict:
|
|
82
|
+
"""Summarize notable GITHUB_TOKEN permission risks."""
|
|
83
|
+
write_permissions = {
|
|
84
|
+
permission.scope
|
|
85
|
+
for permission in perms
|
|
86
|
+
if permission.access.lower() == "write"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"has_write_all": all(
|
|
91
|
+
scope in write_permissions for scope in KNOWN_PERMISSION_SCOPES
|
|
92
|
+
),
|
|
93
|
+
"has_code_write": "contents" in write_permissions,
|
|
94
|
+
"has_workflow_write": "actions" in write_permissions,
|
|
95
|
+
"has_pr_write": "pull-requests" in write_permissions,
|
|
96
|
+
"has_package_write": "packages" in write_permissions,
|
|
97
|
+
"overall_risk": max(
|
|
98
|
+
(permission.risk_level for permission in perms),
|
|
99
|
+
default=RiskLevel.INFO,
|
|
100
|
+
),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _permissions_to_findings(
|
|
105
|
+
permissions: Any,
|
|
106
|
+
workflow_file: str,
|
|
107
|
+
job_name: str,
|
|
108
|
+
oidc_expected: bool,
|
|
109
|
+
) -> list[GitHubTokenPermission]:
|
|
110
|
+
if permissions is None or permissions == {}:
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
if isinstance(permissions, str):
|
|
114
|
+
access = permissions.strip().lower()
|
|
115
|
+
if access == "write-all":
|
|
116
|
+
return [
|
|
117
|
+
GitHubTokenPermission(
|
|
118
|
+
workflow_file=workflow_file,
|
|
119
|
+
job_name=job_name,
|
|
120
|
+
scope=scope,
|
|
121
|
+
access="write",
|
|
122
|
+
risk_level=RiskLevel.HIGH,
|
|
123
|
+
)
|
|
124
|
+
for scope in KNOWN_PERMISSION_SCOPES
|
|
125
|
+
]
|
|
126
|
+
if access == "read-all":
|
|
127
|
+
return [
|
|
128
|
+
GitHubTokenPermission(
|
|
129
|
+
workflow_file=workflow_file,
|
|
130
|
+
job_name=job_name,
|
|
131
|
+
scope=scope,
|
|
132
|
+
access="read",
|
|
133
|
+
risk_level=RiskLevel.LOW,
|
|
134
|
+
)
|
|
135
|
+
for scope in KNOWN_PERMISSION_SCOPES
|
|
136
|
+
]
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
if not isinstance(permissions, dict):
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
findings: list[GitHubTokenPermission] = []
|
|
143
|
+
for scope, access in permissions.items():
|
|
144
|
+
normalized_scope = str(scope).strip().lower()
|
|
145
|
+
normalized_access = str(access).strip().lower()
|
|
146
|
+
findings.append(
|
|
147
|
+
GitHubTokenPermission(
|
|
148
|
+
workflow_file=workflow_file,
|
|
149
|
+
job_name=job_name,
|
|
150
|
+
scope=normalized_scope,
|
|
151
|
+
access=normalized_access,
|
|
152
|
+
risk_level=_classify_permission(
|
|
153
|
+
normalized_scope,
|
|
154
|
+
normalized_access,
|
|
155
|
+
oidc_expected,
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
return findings
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _classify_permission(scope: str, access: str, oidc_expected: bool) -> RiskLevel:
|
|
163
|
+
if access != "write":
|
|
164
|
+
return RiskLevel.LOW
|
|
165
|
+
|
|
166
|
+
if scope == "id-token" and oidc_expected:
|
|
167
|
+
return RiskLevel.INFO
|
|
168
|
+
|
|
169
|
+
if scope in HIGH_WRITE_SCOPES:
|
|
170
|
+
return RiskLevel.HIGH
|
|
171
|
+
|
|
172
|
+
if scope in MEDIUM_WRITE_SCOPES:
|
|
173
|
+
return RiskLevel.MEDIUM
|
|
174
|
+
|
|
175
|
+
return RiskLevel.LOW
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _workflow_uses_role_to_assume(workflow_data: dict) -> bool:
|
|
179
|
+
jobs = workflow_data.get("jobs")
|
|
180
|
+
if jobs is None:
|
|
181
|
+
jobs = {}
|
|
182
|
+
if not isinstance(jobs, dict):
|
|
183
|
+
return False
|
|
184
|
+
return any(
|
|
185
|
+
isinstance(job_data, dict) and _job_uses_role_to_assume(job_data)
|
|
186
|
+
for job_data in jobs.values()
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _job_uses_role_to_assume(job_data: dict) -> bool:
|
|
191
|
+
steps = job_data.get("steps", [])
|
|
192
|
+
if not isinstance(steps, list):
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
for step in steps:
|
|
196
|
+
if not isinstance(step, dict):
|
|
197
|
+
continue
|
|
198
|
+
step_with = step.get("with", {})
|
|
199
|
+
if isinstance(step_with, dict) and step_with.get("role-to-assume"):
|
|
200
|
+
return True
|
|
201
|
+
return False
|