skill-scanner 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.
- skill_scanner-0.1.0/.github/workflows/ci.yml +64 -0
- skill_scanner-0.1.0/.github/workflows/publish-testpypi.yml +49 -0
- skill_scanner-0.1.0/.github/workflows/release.yml +39 -0
- skill_scanner-0.1.0/.github/workflows/zizmor.yml +28 -0
- skill_scanner-0.1.0/.gitignore +18 -0
- skill_scanner-0.1.0/.python-version +1 -0
- skill_scanner-0.1.0/PKG-INFO +156 -0
- skill_scanner-0.1.0/README.md +136 -0
- skill_scanner-0.1.0/pyproject.toml +58 -0
- skill_scanner-0.1.0/renovate.json +75 -0
- skill_scanner-0.1.0/src/skill_scanner/__init__.py +4 -0
- skill_scanner-0.1.0/src/skill_scanner/__main__.py +6 -0
- skill_scanner-0.1.0/src/skill_scanner/analyzers/__init__.py +5 -0
- skill_scanner-0.1.0/src/skill_scanner/analyzers/ai_analyzer.py +46 -0
- skill_scanner-0.1.0/src/skill_scanner/analyzers/pipeline.py +104 -0
- skill_scanner-0.1.0/src/skill_scanner/analyzers/vt_analyzer.py +114 -0
- skill_scanner-0.1.0/src/skill_scanner/cli.py +229 -0
- skill_scanner-0.1.0/src/skill_scanner/config.py +58 -0
- skill_scanner-0.1.0/src/skill_scanner/discovery/__init__.py +5 -0
- skill_scanner-0.1.0/src/skill_scanner/discovery/finder.py +356 -0
- skill_scanner-0.1.0/src/skill_scanner/discovery/patterns.py +62 -0
- skill_scanner-0.1.0/src/skill_scanner/models/__init__.py +21 -0
- skill_scanner-0.1.0/src/skill_scanner/models/findings.py +38 -0
- skill_scanner-0.1.0/src/skill_scanner/models/reports.py +49 -0
- skill_scanner-0.1.0/src/skill_scanner/models/targets.py +55 -0
- skill_scanner-0.1.0/src/skill_scanner/output/__init__.py +14 -0
- skill_scanner-0.1.0/src/skill_scanner/output/console.py +36 -0
- skill_scanner-0.1.0/src/skill_scanner/output/json_export.py +12 -0
- skill_scanner-0.1.0/src/skill_scanner/output/sarif_export.py +66 -0
- skill_scanner-0.1.0/src/skill_scanner/output/summary.py +110 -0
- skill_scanner-0.1.0/src/skill_scanner/providers/__init__.py +17 -0
- skill_scanner-0.1.0/src/skill_scanner/providers/base.py +41 -0
- skill_scanner-0.1.0/src/skill_scanner/providers/openai_provider.py +86 -0
- skill_scanner-0.1.0/src/skill_scanner/scoring/__init__.py +5 -0
- skill_scanner-0.1.0/src/skill_scanner/scoring/risk.py +60 -0
- skill_scanner-0.1.0/src/skill_scanner/utils/retry.py +34 -0
- skill_scanner-0.1.0/src/skill_scanner/validation/__init__.py +7 -0
- skill_scanner-0.1.0/src/skill_scanner/validation/frontmatter.py +24 -0
- skill_scanner-0.1.0/src/skill_scanner/validation/skill_spec.py +105 -0
- skill_scanner-0.1.0/src/skill_scanner/validation/static_rules.py +77 -0
- skill_scanner-0.1.0/tests/conftest.py +10 -0
- skill_scanner-0.1.0/tests/fixtures/encoded_payload_skill/scripts/run.sh +1 -0
- skill_scanner-0.1.0/tests/fixtures/exfil_skill/scripts/exfil.py +3 -0
- skill_scanner-0.1.0/tests/test_ai_analyzer.py +71 -0
- skill_scanner-0.1.0/tests/test_cli.py +187 -0
- skill_scanner-0.1.0/tests/test_discovery.py +46 -0
- skill_scanner-0.1.0/tests/test_pipeline.py +65 -0
- skill_scanner-0.1.0/tests/test_scoring.py +62 -0
- skill_scanner-0.1.0/tests/test_validation.py +50 -0
- skill_scanner-0.1.0/tests/test_vt_analyzer.py +68 -0
- skill_scanner-0.1.0/uv.lock +1520 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "src/**"
|
|
8
|
+
- "tests/**"
|
|
9
|
+
- "pyproject.toml"
|
|
10
|
+
- "uv.lock"
|
|
11
|
+
- ".github/workflows/**"
|
|
12
|
+
pull_request:
|
|
13
|
+
paths:
|
|
14
|
+
- "src/**"
|
|
15
|
+
- "tests/**"
|
|
16
|
+
- "pyproject.toml"
|
|
17
|
+
- "uv.lock"
|
|
18
|
+
- ".github/workflows/**"
|
|
19
|
+
|
|
20
|
+
concurrency:
|
|
21
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
22
|
+
cancel-in-progress: true
|
|
23
|
+
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
|
|
27
|
+
jobs:
|
|
28
|
+
trusted-publishing-config:
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
- name: Validate trusted publishing workflows
|
|
35
|
+
run: |
|
|
36
|
+
set -euo pipefail
|
|
37
|
+
test -f .github/workflows/publish-testpypi.yml
|
|
38
|
+
test -f .github/workflows/release.yml
|
|
39
|
+
grep -Fq 'name: test' .github/workflows/publish-testpypi.yml
|
|
40
|
+
grep -Fq 'name: release' .github/workflows/release.yml
|
|
41
|
+
grep -Fq 'id-token: write' .github/workflows/publish-testpypi.yml
|
|
42
|
+
grep -Fq 'id-token: write' .github/workflows/release.yml
|
|
43
|
+
grep -Fq 'gh-action-pypi-publish@' .github/workflows/publish-testpypi.yml
|
|
44
|
+
grep -Fq 'gh-action-pypi-publish@' .github/workflows/release.yml
|
|
45
|
+
grep -Fq 'repository-url: https://test.pypi.org/legacy/' .github/workflows/publish-testpypi.yml
|
|
46
|
+
|
|
47
|
+
test:
|
|
48
|
+
needs: trusted-publishing-config
|
|
49
|
+
runs-on: ubuntu-latest
|
|
50
|
+
strategy:
|
|
51
|
+
matrix:
|
|
52
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
55
|
+
with:
|
|
56
|
+
persist-credentials: false
|
|
57
|
+
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
|
58
|
+
with:
|
|
59
|
+
python-version: ${{ matrix.python-version }}
|
|
60
|
+
enable-cache: true
|
|
61
|
+
- run: uv sync --locked --extra all --group dev
|
|
62
|
+
- run: uv run ruff check .
|
|
63
|
+
- run: uv run mypy src
|
|
64
|
+
- run: uv run pytest --cov=src --cov-report=xml
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Publish TestPyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "src/**"
|
|
8
|
+
- "pyproject.toml"
|
|
9
|
+
- "uv.lock"
|
|
10
|
+
- "README.md"
|
|
11
|
+
- ".github/workflows/publish-testpypi.yml"
|
|
12
|
+
- ".github/workflows/release.yml"
|
|
13
|
+
workflow_dispatch:
|
|
14
|
+
|
|
15
|
+
concurrency:
|
|
16
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
test-pypi-publish:
|
|
24
|
+
name: Publish to TestPyPI
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
environment:
|
|
27
|
+
name: test
|
|
28
|
+
url: https://test.pypi.org/p/skill-scanner
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write
|
|
31
|
+
attestations: write
|
|
32
|
+
contents: read
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
35
|
+
with:
|
|
36
|
+
persist-credentials: false
|
|
37
|
+
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
|
38
|
+
with:
|
|
39
|
+
python-version: "3.12"
|
|
40
|
+
enable-cache: false
|
|
41
|
+
- run: uv sync --locked --group dev
|
|
42
|
+
- run: uv build --no-sources
|
|
43
|
+
- name: Attest build provenance
|
|
44
|
+
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
|
45
|
+
with:
|
|
46
|
+
subject-path: "dist/*"
|
|
47
|
+
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
|
48
|
+
with:
|
|
49
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Publish PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
9
|
+
cancel-in-progress: false
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
pypi-publish:
|
|
16
|
+
name: Publish to PyPI
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
environment:
|
|
19
|
+
name: release
|
|
20
|
+
url: https://pypi.org/p/skill-scanner
|
|
21
|
+
permissions:
|
|
22
|
+
id-token: write
|
|
23
|
+
attestations: write
|
|
24
|
+
contents: read
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
27
|
+
with:
|
|
28
|
+
persist-credentials: false
|
|
29
|
+
- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
|
30
|
+
with:
|
|
31
|
+
python-version: "3.12"
|
|
32
|
+
enable-cache: false
|
|
33
|
+
- run: uv sync --locked --group dev
|
|
34
|
+
- run: uv build --no-sources
|
|
35
|
+
- name: Attest build provenance
|
|
36
|
+
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
|
|
37
|
+
with:
|
|
38
|
+
subject-path: "dist/*"
|
|
39
|
+
- uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: GitHub Actions Security Analysis with zizmor
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: ["**"]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions: {}
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
zizmor:
|
|
17
|
+
name: Run zizmor
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
permissions:
|
|
20
|
+
security-events: write
|
|
21
|
+
steps:
|
|
22
|
+
- name: Checkout repository
|
|
23
|
+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
24
|
+
with:
|
|
25
|
+
persist-credentials: false
|
|
26
|
+
|
|
27
|
+
- name: Run zizmor
|
|
28
|
+
uses: zizmorcore/zizmor-action@135698455da5c3b3e55f73f4419e481ab68cdd95 # v0.4.1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: skill-scanner
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Security scanner for AI agent skills and instruction artifacts
|
|
5
|
+
Author: skill-scanner
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: httpx==0.28.1
|
|
8
|
+
Requires-Dist: pydantic==2.12.5
|
|
9
|
+
Requires-Dist: pyyaml==6.0.3
|
|
10
|
+
Requires-Dist: rich==14.3.2
|
|
11
|
+
Requires-Dist: typer==0.24.0
|
|
12
|
+
Provides-Extra: all
|
|
13
|
+
Requires-Dist: openai==2.21.0; extra == 'all'
|
|
14
|
+
Requires-Dist: vt-py==0.22.0; extra == 'all'
|
|
15
|
+
Provides-Extra: openai
|
|
16
|
+
Requires-Dist: openai==2.21.0; extra == 'openai'
|
|
17
|
+
Provides-Extra: virustotal
|
|
18
|
+
Requires-Dist: vt-py==0.22.0; extra == 'virustotal'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# skill-scanner
|
|
22
|
+
|
|
23
|
+
`skill-scanner` reviews AI skill and instruction artifacts for security risk using:
|
|
24
|
+
- OpenAI analysis
|
|
25
|
+
- VirusTotal analysis
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- Python 3.11+
|
|
30
|
+
- [`uv`](https://docs.astral.sh/uv/)
|
|
31
|
+
- OpenAI and/or VirusTotal API key (at least one)
|
|
32
|
+
|
|
33
|
+
## Install (from source)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv sync --all-extras --group dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Run with:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
uv run skill-scanner --help
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Alias:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv run skillscan --help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## What gets scanned
|
|
52
|
+
|
|
53
|
+
By default, `discover` and `scan` detect common skill/instruction files (for example `SKILL.md`, `AGENTS.md`, `*.instructions.md`, `*.prompt.md`, `.mdc`, and related artifacts).
|
|
54
|
+
|
|
55
|
+
Use `--path` to target a specific file or folder.
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# See targets
|
|
61
|
+
uv run skill-scanner discover --format json
|
|
62
|
+
|
|
63
|
+
# Verify key/model configuration
|
|
64
|
+
uv run skill-scanner doctor
|
|
65
|
+
|
|
66
|
+
# Run a combined scan (if both keys are configured)
|
|
67
|
+
uv run skill-scanner scan --format summary
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Key configuration and analyzer selection
|
|
71
|
+
|
|
72
|
+
`scan` requires at least one analyzer enabled.
|
|
73
|
+
|
|
74
|
+
- If only `OPENAI_API_KEY` is available, AI runs and VT is disabled.
|
|
75
|
+
- If only `VT_API_KEY` is available, VT runs and AI is disabled.
|
|
76
|
+
- If both keys are available, VT findings are included and VT context is passed into AI analysis.
|
|
77
|
+
- You can disable either analyzer with `--no-ai` or `--no-vt`.
|
|
78
|
+
|
|
79
|
+
## API key safety
|
|
80
|
+
|
|
81
|
+
Never commit API keys. This repository ignores `.env` by default.
|
|
82
|
+
|
|
83
|
+
### Option 1: Shell environment variables
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
export OPENAI_API_KEY="..."
|
|
87
|
+
export VT_API_KEY="..."
|
|
88
|
+
uv run skill-scanner scan --format summary
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Option 2: Local `.env` file
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
OPENAI_API_KEY=...
|
|
95
|
+
VT_API_KEY=...
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Then run:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
uv run skill-scanner doctor
|
|
102
|
+
uv run skill-scanner scan --format summary
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Option 3: 1Password secret references (recommended)
|
|
106
|
+
|
|
107
|
+
Use 1Password secret references instead of plaintext secrets in `.env`:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
OPENAI_API_KEY=op://Engineering/OpenAI/api_key
|
|
111
|
+
VT_API_KEY=op://Engineering/VirusTotal/api_key
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Run the scanner through 1Password CLI so references are resolved at runtime:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
op run --env-file=.env -- uv run skill-scanner scan --format summary
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Security best practice:
|
|
121
|
+
- Prefer a 1Password Service Account scoped to only the vault/items required for scanning (least privilege).
|
|
122
|
+
|
|
123
|
+
Reference:
|
|
124
|
+
- https://developer.1password.com/docs/cli/secret-references/
|
|
125
|
+
|
|
126
|
+
## Output formats
|
|
127
|
+
|
|
128
|
+
`scan --format` supports:
|
|
129
|
+
- `table` (default)
|
|
130
|
+
- `summary`
|
|
131
|
+
- `json`
|
|
132
|
+
- `sarif`
|
|
133
|
+
|
|
134
|
+
You can write output to a file with `--output <path>`.
|
|
135
|
+
|
|
136
|
+
## Useful commands
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# List providers
|
|
140
|
+
uv run skill-scanner providers
|
|
141
|
+
|
|
142
|
+
# Scan one path only
|
|
143
|
+
uv run skill-scanner scan --path ./some/skill/folder --format summary
|
|
144
|
+
|
|
145
|
+
# Filter to medium+
|
|
146
|
+
uv run skill-scanner scan --min-severity medium --format summary
|
|
147
|
+
|
|
148
|
+
# Non-zero exit if high+ findings exist
|
|
149
|
+
uv run skill-scanner scan --fail-on high --format summary
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Exit behavior
|
|
153
|
+
|
|
154
|
+
- `0`: scan completed and fail threshold not hit
|
|
155
|
+
- `1`: `--fail-on` threshold matched
|
|
156
|
+
- `2`: no analyzers enabled (for example missing keys combined with flags)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# skill-scanner
|
|
2
|
+
|
|
3
|
+
`skill-scanner` reviews AI skill and instruction artifacts for security risk using:
|
|
4
|
+
- OpenAI analysis
|
|
5
|
+
- VirusTotal analysis
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python 3.11+
|
|
10
|
+
- [`uv`](https://docs.astral.sh/uv/)
|
|
11
|
+
- OpenAI and/or VirusTotal API key (at least one)
|
|
12
|
+
|
|
13
|
+
## Install (from source)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uv sync --all-extras --group dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run with:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv run skill-scanner --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Alias:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv run skillscan --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What gets scanned
|
|
32
|
+
|
|
33
|
+
By default, `discover` and `scan` detect common skill/instruction files (for example `SKILL.md`, `AGENTS.md`, `*.instructions.md`, `*.prompt.md`, `.mdc`, and related artifacts).
|
|
34
|
+
|
|
35
|
+
Use `--path` to target a specific file or folder.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# See targets
|
|
41
|
+
uv run skill-scanner discover --format json
|
|
42
|
+
|
|
43
|
+
# Verify key/model configuration
|
|
44
|
+
uv run skill-scanner doctor
|
|
45
|
+
|
|
46
|
+
# Run a combined scan (if both keys are configured)
|
|
47
|
+
uv run skill-scanner scan --format summary
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Key configuration and analyzer selection
|
|
51
|
+
|
|
52
|
+
`scan` requires at least one analyzer enabled.
|
|
53
|
+
|
|
54
|
+
- If only `OPENAI_API_KEY` is available, AI runs and VT is disabled.
|
|
55
|
+
- If only `VT_API_KEY` is available, VT runs and AI is disabled.
|
|
56
|
+
- If both keys are available, VT findings are included and VT context is passed into AI analysis.
|
|
57
|
+
- You can disable either analyzer with `--no-ai` or `--no-vt`.
|
|
58
|
+
|
|
59
|
+
## API key safety
|
|
60
|
+
|
|
61
|
+
Never commit API keys. This repository ignores `.env` by default.
|
|
62
|
+
|
|
63
|
+
### Option 1: Shell environment variables
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
export OPENAI_API_KEY="..."
|
|
67
|
+
export VT_API_KEY="..."
|
|
68
|
+
uv run skill-scanner scan --format summary
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Option 2: Local `.env` file
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
OPENAI_API_KEY=...
|
|
75
|
+
VT_API_KEY=...
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
uv run skill-scanner doctor
|
|
82
|
+
uv run skill-scanner scan --format summary
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Option 3: 1Password secret references (recommended)
|
|
86
|
+
|
|
87
|
+
Use 1Password secret references instead of plaintext secrets in `.env`:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
OPENAI_API_KEY=op://Engineering/OpenAI/api_key
|
|
91
|
+
VT_API_KEY=op://Engineering/VirusTotal/api_key
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Run the scanner through 1Password CLI so references are resolved at runtime:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
op run --env-file=.env -- uv run skill-scanner scan --format summary
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Security best practice:
|
|
101
|
+
- Prefer a 1Password Service Account scoped to only the vault/items required for scanning (least privilege).
|
|
102
|
+
|
|
103
|
+
Reference:
|
|
104
|
+
- https://developer.1password.com/docs/cli/secret-references/
|
|
105
|
+
|
|
106
|
+
## Output formats
|
|
107
|
+
|
|
108
|
+
`scan --format` supports:
|
|
109
|
+
- `table` (default)
|
|
110
|
+
- `summary`
|
|
111
|
+
- `json`
|
|
112
|
+
- `sarif`
|
|
113
|
+
|
|
114
|
+
You can write output to a file with `--output <path>`.
|
|
115
|
+
|
|
116
|
+
## Useful commands
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# List providers
|
|
120
|
+
uv run skill-scanner providers
|
|
121
|
+
|
|
122
|
+
# Scan one path only
|
|
123
|
+
uv run skill-scanner scan --path ./some/skill/folder --format summary
|
|
124
|
+
|
|
125
|
+
# Filter to medium+
|
|
126
|
+
uv run skill-scanner scan --min-severity medium --format summary
|
|
127
|
+
|
|
128
|
+
# Non-zero exit if high+ findings exist
|
|
129
|
+
uv run skill-scanner scan --fail-on high --format summary
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Exit behavior
|
|
133
|
+
|
|
134
|
+
- `0`: scan completed and fail threshold not hit
|
|
135
|
+
- `1`: `--fail-on` threshold matched
|
|
136
|
+
- `2`: no analyzers enabled (for example missing keys combined with flags)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "skill-scanner"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Security scanner for AI agent skills and instruction artifacts"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "skill-scanner" }]
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"httpx==0.28.1",
|
|
10
|
+
"pydantic==2.12.5",
|
|
11
|
+
"pyyaml==6.0.3",
|
|
12
|
+
"rich==14.3.2",
|
|
13
|
+
"typer==0.24.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
openai = ["openai==2.21.0"]
|
|
18
|
+
virustotal = ["vt-py==0.22.0"]
|
|
19
|
+
all = ["openai==2.21.0", "vt-py==0.22.0"]
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"mypy==1.19.1",
|
|
24
|
+
"pytest==9.0.2",
|
|
25
|
+
"pytest-cov==7.0.0",
|
|
26
|
+
"ruff==0.15.1",
|
|
27
|
+
"types-PyYAML==6.0.12.20250915",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
skill-scanner = "skill_scanner.cli:app"
|
|
32
|
+
skillscan = "skill_scanner.cli:app"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling==1.27.0"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/skill_scanner"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
line-length = 100
|
|
43
|
+
target-version = "py311"
|
|
44
|
+
src = ["src", "tests"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff.lint]
|
|
47
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
48
|
+
ignore = ["B008", "E501", "SIM108", "SIM115"]
|
|
49
|
+
|
|
50
|
+
[tool.mypy]
|
|
51
|
+
python_version = "3.11"
|
|
52
|
+
strict = true
|
|
53
|
+
warn_unused_configs = true
|
|
54
|
+
mypy_path = "src"
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
testpaths = ["tests"]
|
|
58
|
+
addopts = "-q"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": [
|
|
4
|
+
"config:best-practices",
|
|
5
|
+
"helpers:pinGitHubActionDigests",
|
|
6
|
+
"helpers:pinGitHubActionDigestsToSemver",
|
|
7
|
+
"schedule:weekly"
|
|
8
|
+
],
|
|
9
|
+
"dependencyDashboard": true,
|
|
10
|
+
"minimumReleaseAge": "7 days",
|
|
11
|
+
"rangeStrategy": "pin",
|
|
12
|
+
"lockFileMaintenance": {
|
|
13
|
+
"enabled": true,
|
|
14
|
+
"schedule": [
|
|
15
|
+
"before 5am on monday"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"packageRules": [
|
|
19
|
+
{
|
|
20
|
+
"groupName": "github-actions",
|
|
21
|
+
"matchManagers": [
|
|
22
|
+
"github-actions"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"groupName": "Testing Framework",
|
|
27
|
+
"matchDatasources": [
|
|
28
|
+
"pypi"
|
|
29
|
+
],
|
|
30
|
+
"matchPackageNames": [
|
|
31
|
+
"pytest",
|
|
32
|
+
"/^pytest-/"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"groupName": "Type Checking & Validation",
|
|
37
|
+
"matchDatasources": [
|
|
38
|
+
"pypi"
|
|
39
|
+
],
|
|
40
|
+
"matchPackageNames": [
|
|
41
|
+
"mypy",
|
|
42
|
+
"types-PyYAML",
|
|
43
|
+
"pydantic",
|
|
44
|
+
"pydantic-core"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"groupName": "Dev Tools",
|
|
49
|
+
"matchDatasources": [
|
|
50
|
+
"pypi"
|
|
51
|
+
],
|
|
52
|
+
"matchPackageNames": [
|
|
53
|
+
"ruff"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"groupName": "Runtime Dependencies",
|
|
58
|
+
"matchDatasources": [
|
|
59
|
+
"pypi"
|
|
60
|
+
],
|
|
61
|
+
"matchPackageNames": [
|
|
62
|
+
"httpx",
|
|
63
|
+
"openai",
|
|
64
|
+
"typer",
|
|
65
|
+
"vt-py"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"matchUpdateTypes": [
|
|
70
|
+
"major"
|
|
71
|
+
],
|
|
72
|
+
"dependencyDashboardApproval": true
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from skill_scanner.models.reports import AIReport, VTReport
|
|
6
|
+
from skill_scanner.models.targets import ScanTarget
|
|
7
|
+
from skill_scanner.providers.base import LLMProvider
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_payload(target: ScanTarget, max_chars: int = 400_000) -> str:
|
|
11
|
+
chunks: list[str] = []
|
|
12
|
+
total = 0
|
|
13
|
+
for meta in target.files:
|
|
14
|
+
path = Path(meta.path)
|
|
15
|
+
try:
|
|
16
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
17
|
+
except Exception:
|
|
18
|
+
continue
|
|
19
|
+
chunk = f"\n## FILE: {meta.relative_path}\n{text}\n"
|
|
20
|
+
if total + len(chunk) > max_chars:
|
|
21
|
+
continue
|
|
22
|
+
chunks.append(chunk)
|
|
23
|
+
total += len(chunk)
|
|
24
|
+
return "".join(chunks)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _append_vt_context(payload: str, vt_report: VTReport | None) -> str:
|
|
28
|
+
if vt_report is None:
|
|
29
|
+
return payload
|
|
30
|
+
vt_context = (
|
|
31
|
+
"\n## VIRUSTOTAL_CONTEXT\n"
|
|
32
|
+
f"sha256: {vt_report.sha256}\n"
|
|
33
|
+
f"malicious: {vt_report.malicious}\n"
|
|
34
|
+
f"suspicious: {vt_report.suspicious}\n"
|
|
35
|
+
f"harmless: {vt_report.harmless}\n"
|
|
36
|
+
f"undetected: {vt_report.undetected}\n"
|
|
37
|
+
f"permalink: {vt_report.permalink or 'n/a'}\n"
|
|
38
|
+
)
|
|
39
|
+
return f"{payload}{vt_context}"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def analyze_with_ai(target: ScanTarget, provider: LLMProvider, vt_report: VTReport | None = None) -> AIReport:
|
|
43
|
+
payload = build_payload(target)
|
|
44
|
+
if not payload.strip():
|
|
45
|
+
return AIReport(provider=provider.name, model=provider.model, findings=[])
|
|
46
|
+
return provider.analyze(target, _append_vt_context(payload, vt_report))
|