ghostrules 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.
- ghostrules-0.1.0/.ghostrules.toml +10 -0
- ghostrules-0.1.0/.gitattributes +5 -0
- ghostrules-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- ghostrules-0.1.0/.github/ISSUE_TEMPLATE/false-positive.yml +49 -0
- ghostrules-0.1.0/.github/ISSUE_TEMPLATE/new-rule.yml +52 -0
- ghostrules-0.1.0/.github/workflows/ci.yml +40 -0
- ghostrules-0.1.0/.github/workflows/release.yml +77 -0
- ghostrules-0.1.0/.github/workflows/self-test.yml +73 -0
- ghostrules-0.1.0/.gitignore +14 -0
- ghostrules-0.1.0/.pre-commit-hooks.yaml +9 -0
- ghostrules-0.1.0/CHANGELOG.md +41 -0
- ghostrules-0.1.0/CLAUDE.md +60 -0
- ghostrules-0.1.0/CONTRIBUTING.md +80 -0
- ghostrules-0.1.0/LICENSE +21 -0
- ghostrules-0.1.0/PKG-INFO +333 -0
- ghostrules-0.1.0/README.md +278 -0
- ghostrules-0.1.0/SECURITY.md +48 -0
- ghostrules-0.1.0/action.yml +97 -0
- ghostrules-0.1.0/demo/demo.tape +39 -0
- ghostrules-0.1.0/docs/DECISIONS.md +320 -0
- ghostrules-0.1.0/docs/PLAN.md +101 -0
- ghostrules-0.1.0/docs/RULES.md +147 -0
- ghostrules-0.1.0/docs/SPEC.md +158 -0
- ghostrules-0.1.0/examples/clean-repo/.claude/settings.json +12 -0
- ghostrules-0.1.0/examples/clean-repo/.mcp.json +25 -0
- ghostrules-0.1.0/examples/clean-repo/AGENTS.md +11 -0
- ghostrules-0.1.0/examples/clean-repo/CLAUDE.md +5 -0
- ghostrules-0.1.0/examples/clean-repo/README.md +7 -0
- ghostrules-0.1.0/examples/poisoned-repo/.claude/settings.json +9 -0
- ghostrules-0.1.0/examples/poisoned-repo/.claude/skills/setup/SKILL.md +9 -0
- ghostrules-0.1.0/examples/poisoned-repo/.cursorrules +4 -0
- ghostrules-0.1.0/examples/poisoned-repo/.github/copilot-instructions.md +7 -0
- ghostrules-0.1.0/examples/poisoned-repo/.mcp.json +42 -0
- ghostrules-0.1.0/examples/poisoned-repo/AGENTS.md +6 -0
- ghostrules-0.1.0/examples/poisoned-repo/CLAUDE.md +5 -0
- ghostrules-0.1.0/examples/poisoned-repo/README.md +24 -0
- ghostrules-0.1.0/pyproject.toml +71 -0
- ghostrules-0.1.0/release-notes.md +32 -0
- ghostrules-0.1.0/scripts/changelog_section.py +48 -0
- ghostrules-0.1.0/scripts/gen_rules_table.py +65 -0
- ghostrules-0.1.0/scripts/make_demo.py +278 -0
- ghostrules-0.1.0/scripts/make_fixtures.py +204 -0
- ghostrules-0.1.0/scripts/set_owner.py +72 -0
- ghostrules-0.1.0/src/ghostrules/__init__.py +6 -0
- ghostrules-0.1.0/src/ghostrules/_compat.py +12 -0
- ghostrules-0.1.0/src/ghostrules/clean.py +41 -0
- ghostrules-0.1.0/src/ghostrules/cli.py +319 -0
- ghostrules-0.1.0/src/ghostrules/config.py +93 -0
- ghostrules-0.1.0/src/ghostrules/jsonloc.py +194 -0
- ghostrules-0.1.0/src/ghostrules/markdown_utils.py +59 -0
- ghostrules-0.1.0/src/ghostrules/mcp_config.py +243 -0
- ghostrules-0.1.0/src/ghostrules/models.py +188 -0
- ghostrules-0.1.0/src/ghostrules/py.typed +0 -0
- ghostrules-0.1.0/src/ghostrules/reporters/__init__.py +1 -0
- ghostrules-0.1.0/src/ghostrules/reporters/common.py +86 -0
- ghostrules-0.1.0/src/ghostrules/reporters/json_.py +45 -0
- ghostrules-0.1.0/src/ghostrules/reporters/markdown.py +55 -0
- ghostrules-0.1.0/src/ghostrules/reporters/sarif.py +137 -0
- ghostrules-0.1.0/src/ghostrules/reporters/table.py +235 -0
- ghostrules-0.1.0/src/ghostrules/reveal.py +90 -0
- ghostrules-0.1.0/src/ghostrules/rules/__init__.py +16 -0
- ghostrules-0.1.0/src/ghostrules/rules/agent_settings.py +159 -0
- ghostrules-0.1.0/src/ghostrules/rules/helpers.py +118 -0
- ghostrules-0.1.0/src/ghostrules/rules/injection.py +317 -0
- ghostrules-0.1.0/src/ghostrules/rules/mcp.py +445 -0
- ghostrules-0.1.0/src/ghostrules/rules/patterns.py +187 -0
- ghostrules-0.1.0/src/ghostrules/rules/registry.py +78 -0
- ghostrules-0.1.0/src/ghostrules/rules/unicode.py +503 -0
- ghostrules-0.1.0/src/ghostrules/scanner.py +244 -0
- ghostrules-0.1.0/src/ghostrules/secrets.py +219 -0
- ghostrules-0.1.0/src/ghostrules/targets.py +220 -0
- ghostrules-0.1.0/src/ghostrules/unicode_utils.py +138 -0
- ghostrules-0.1.0/tests/conftest.py +43 -0
- ghostrules-0.1.0/tests/data/sarif-schema-2.1.0.json +3389 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr001_flag_forged/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr001_flag_ok/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr001_tags/AGENTS.md +4 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr002_bidi/AGENTS.md +2 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr002_rtl_medium/AGENTS.md +3 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr003_bom_midfile/AGENTS.md +2 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr003_bom_ok/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr003_emoji_zwj_ok/AGENTS.md +2 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr003_indic_ok/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr003_persian_ok/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr003_soft_hyphen/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr003_zw_file_high/AGENTS.md +20 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr003_zw_line_high/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr003_zw_medium/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr004_vs_bytes/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr004_vs_ok/AGENTS.md +2 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr005_benign_ok/CLAUDE.md +2 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr005_comment_imperative/CLAUDE.md +2 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr005_comment_injection/CLAUDE.md +3 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr005_comment_multiline/CLAUDE.md +5 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr005_in_code_block_ok/CLAUDE.md +4 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr005_short_ok/CLAUDE.md +2 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr005_unterminated/CLAUDE.md +4 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr006_ok/AGENTS.md +3 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr006_url/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/fixtures/unicode/gr006_word/AGENTS.md +1 -0
- ghostrules-0.1.0/tests/secret_samples.py +36 -0
- ghostrules-0.1.0/tests/test_ci_files.py +372 -0
- ghostrules-0.1.0/tests/test_cli.py +409 -0
- ghostrules-0.1.0/tests/test_config.py +75 -0
- ghostrules-0.1.0/tests/test_demo.py +187 -0
- ghostrules-0.1.0/tests/test_docs.py +203 -0
- ghostrules-0.1.0/tests/test_markdown_utils.py +67 -0
- ghostrules-0.1.0/tests/test_mcp_config.py +154 -0
- ghostrules-0.1.0/tests/test_models.py +39 -0
- ghostrules-0.1.0/tests/test_patterns.py +96 -0
- ghostrules-0.1.0/tests/test_reporters.py +345 -0
- ghostrules-0.1.0/tests/test_rules_config.py +491 -0
- ghostrules-0.1.0/tests/test_rules_injection.py +415 -0
- ghostrules-0.1.0/tests/test_rules_unicode.py +319 -0
- ghostrules-0.1.0/tests/test_scanner.py +363 -0
- ghostrules-0.1.0/tests/test_secret_masking.py +256 -0
- ghostrules-0.1.0/tests/test_secrets.py +138 -0
- ghostrules-0.1.0/tests/test_targets.py +183 -0
- ghostrules-0.1.0/tests/test_unicode_utils.py +75 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: False positive
|
|
2
|
+
description: ghostrules reported something that is not a problem.
|
|
3
|
+
title: "[false positive] GRxxx: "
|
|
4
|
+
labels: ["false-positive"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Thanks for reporting it. **Do not paste real secrets or private files.** Reduce the content to
|
|
10
|
+
a small, defanged example (use `example.com`, fake tokens). If the finding involves invisible
|
|
11
|
+
characters, say which ones (for example the output of `ghostrules reveal FILE`).
|
|
12
|
+
- type: input
|
|
13
|
+
id: version
|
|
14
|
+
attributes:
|
|
15
|
+
label: ghostrules version
|
|
16
|
+
description: Output of `ghostrules version`.
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
19
|
+
- type: input
|
|
20
|
+
id: rule
|
|
21
|
+
attributes:
|
|
22
|
+
label: Rule
|
|
23
|
+
description: The rule that fired, for example GR013.
|
|
24
|
+
placeholder: GR013
|
|
25
|
+
validations:
|
|
26
|
+
required: true
|
|
27
|
+
- type: textarea
|
|
28
|
+
id: input
|
|
29
|
+
attributes:
|
|
30
|
+
label: Minimal file that triggers it
|
|
31
|
+
description: The smallest defanged snippet that reproduces the finding, and its file name.
|
|
32
|
+
render: text
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
- type: textarea
|
|
36
|
+
id: output
|
|
37
|
+
attributes:
|
|
38
|
+
label: What ghostrules printed
|
|
39
|
+
description: The finding (`ghostrules scan --no-color FILE`).
|
|
40
|
+
render: text
|
|
41
|
+
validations:
|
|
42
|
+
required: true
|
|
43
|
+
- type: textarea
|
|
44
|
+
id: why
|
|
45
|
+
attributes:
|
|
46
|
+
label: Why it is legitimate
|
|
47
|
+
description: What the text is for, and how a rule could tell it apart from a real problem.
|
|
48
|
+
validations:
|
|
49
|
+
required: true
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
name: New rule proposal
|
|
2
|
+
description: Suggest a new detection, or a missed one (false negative).
|
|
3
|
+
title: "[rule] "
|
|
4
|
+
labels: ["new-rule"]
|
|
5
|
+
body:
|
|
6
|
+
- type: markdown
|
|
7
|
+
attributes:
|
|
8
|
+
value: |
|
|
9
|
+
Good rules are precise: they say what to look for, why it is dangerous in a file an agent
|
|
10
|
+
obeys, and what must **not** match. **Use defanged examples only** (`example.com`, fake
|
|
11
|
+
tokens), never working payloads.
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: threat
|
|
14
|
+
attributes:
|
|
15
|
+
label: The risk
|
|
16
|
+
description: What can an attacker do with it, and which agent or file does it affect? Link a primary source if there is one.
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
19
|
+
- type: textarea
|
|
20
|
+
id: positive
|
|
21
|
+
attributes:
|
|
22
|
+
label: Should be reported
|
|
23
|
+
description: One or more defanged examples that the rule must flag, with the file name.
|
|
24
|
+
render: text
|
|
25
|
+
validations:
|
|
26
|
+
required: true
|
|
27
|
+
- type: textarea
|
|
28
|
+
id: negative
|
|
29
|
+
attributes:
|
|
30
|
+
label: Must not be reported
|
|
31
|
+
description: Legitimate examples that look similar and must stay quiet.
|
|
32
|
+
render: text
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
- type: dropdown
|
|
36
|
+
id: severity
|
|
37
|
+
attributes:
|
|
38
|
+
label: Suggested severity
|
|
39
|
+
options: [critical, high, medium, low, info]
|
|
40
|
+
validations:
|
|
41
|
+
required: true
|
|
42
|
+
- type: textarea
|
|
43
|
+
id: detection
|
|
44
|
+
attributes:
|
|
45
|
+
label: Detection idea
|
|
46
|
+
description: A pattern, a heuristic or a config check. Optional but helpful.
|
|
47
|
+
- type: checkboxes
|
|
48
|
+
id: contribute
|
|
49
|
+
attributes:
|
|
50
|
+
label: Contribution
|
|
51
|
+
options:
|
|
52
|
+
- label: I can send a pull request (see CONTRIBUTING.md).
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
15
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- name: Install
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
- name: Lint
|
|
24
|
+
run: ruff check .
|
|
25
|
+
- name: Format check
|
|
26
|
+
run: ruff format --check .
|
|
27
|
+
- name: Test
|
|
28
|
+
run: pytest -q
|
|
29
|
+
- name: Smoke test the demos
|
|
30
|
+
shell: bash
|
|
31
|
+
run: |
|
|
32
|
+
ghostrules scan examples/clean-repo
|
|
33
|
+
code=0
|
|
34
|
+
ghostrules scan examples/poisoned-repo --no-color > /dev/null || code=$?
|
|
35
|
+
test "$code" = "1"
|
|
36
|
+
- name: ghostrules scans its own repository
|
|
37
|
+
shell: bash
|
|
38
|
+
run: |
|
|
39
|
+
ghostrules scan . --no-color
|
|
40
|
+
ghostrules scan . --all-files --no-color
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Push a tag like v0.1.0 to publish. PyPI uses Trusted Publishing (OIDC): there is no API token
|
|
4
|
+
# in this repository. One-time setup on PyPI: add a "pending publisher" for this repository,
|
|
5
|
+
# workflow "release.yml" and environment "pypi".
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
|
|
23
|
+
- name: The tag must match the package version
|
|
24
|
+
env:
|
|
25
|
+
TAG: ${{ github.ref_name }}
|
|
26
|
+
run: |
|
|
27
|
+
python - <<'PY'
|
|
28
|
+
import os, re
|
|
29
|
+
version = re.search(r'__version__ = "([^"]+)"', open("src/ghostrules/__init__.py").read()).group(1)
|
|
30
|
+
tag = os.environ["TAG"].removeprefix("v")
|
|
31
|
+
assert version == tag, f"tag {tag} does not match __version__ {version}"
|
|
32
|
+
PY
|
|
33
|
+
|
|
34
|
+
- name: The changelog must have release notes for this tag
|
|
35
|
+
env:
|
|
36
|
+
TAG: ${{ github.ref_name }}
|
|
37
|
+
run: python scripts/changelog_section.py "$TAG" > release-notes.md
|
|
38
|
+
|
|
39
|
+
- name: Build
|
|
40
|
+
run: |
|
|
41
|
+
python -m pip install --disable-pip-version-check --quiet build
|
|
42
|
+
python -m build
|
|
43
|
+
|
|
44
|
+
- uses: actions/upload-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: dist
|
|
47
|
+
path: |
|
|
48
|
+
dist/
|
|
49
|
+
release-notes.md
|
|
50
|
+
|
|
51
|
+
publish-pypi:
|
|
52
|
+
needs: build
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
environment: pypi
|
|
55
|
+
permissions:
|
|
56
|
+
id-token: write # Trusted Publishing
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/download-artifact@v4
|
|
59
|
+
with:
|
|
60
|
+
name: dist
|
|
61
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
62
|
+
|
|
63
|
+
github-release:
|
|
64
|
+
needs: publish-pypi
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
permissions:
|
|
67
|
+
contents: write
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/download-artifact@v4
|
|
70
|
+
with:
|
|
71
|
+
name: dist
|
|
72
|
+
- name: Create the GitHub release
|
|
73
|
+
env:
|
|
74
|
+
GH_TOKEN: ${{ github.token }}
|
|
75
|
+
TAG: ${{ github.ref_name }}
|
|
76
|
+
REPO: ${{ github.repository }}
|
|
77
|
+
run: gh release create "$TAG" dist/* --repo "$REPO" --title "$TAG" --notes-file release-notes.md
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: Self-test
|
|
2
|
+
|
|
3
|
+
# Runs the action from this very checkout against the demo repositories.
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
action:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Poisoned demo with fail-on none (must pass, must report findings)
|
|
19
|
+
id: poisoned
|
|
20
|
+
uses: ./
|
|
21
|
+
with:
|
|
22
|
+
path: examples/poisoned-repo
|
|
23
|
+
fail-on: none
|
|
24
|
+
upload-sarif: "false"
|
|
25
|
+
|
|
26
|
+
- name: SARIF must contain results, led by the hidden instruction
|
|
27
|
+
run: |
|
|
28
|
+
python - <<'PY'
|
|
29
|
+
import json
|
|
30
|
+
run = json.load(open("ghostrules.sarif"))["runs"][0]
|
|
31
|
+
results = run["results"]
|
|
32
|
+
assert len(results) > 10, f"expected many results, got {len(results)}"
|
|
33
|
+
ids = {r["ruleId"] for r in results}
|
|
34
|
+
assert {"GR001", "GR012", "GR101"} <= ids, ids
|
|
35
|
+
assert results[0]["ruleId"] == "GR001", results[0]["ruleId"]
|
|
36
|
+
assert run["tool"]["driver"]["rules"], "rules missing"
|
|
37
|
+
print(f"ok: {len(results)} results")
|
|
38
|
+
PY
|
|
39
|
+
|
|
40
|
+
- name: The exit-code output is 0
|
|
41
|
+
env:
|
|
42
|
+
EXIT_CODE: ${{ steps.poisoned.outputs.exit-code }}
|
|
43
|
+
run: test "$EXIT_CODE" = "0"
|
|
44
|
+
|
|
45
|
+
- name: Poisoned demo with the default fail-on (must fail the step)
|
|
46
|
+
id: gate
|
|
47
|
+
uses: ./
|
|
48
|
+
continue-on-error: true
|
|
49
|
+
with:
|
|
50
|
+
path: examples/poisoned-repo
|
|
51
|
+
upload-sarif: "false"
|
|
52
|
+
|
|
53
|
+
- name: The gate must have failed
|
|
54
|
+
env:
|
|
55
|
+
OUTCOME: ${{ steps.gate.outcome }}
|
|
56
|
+
EXIT_CODE: ${{ steps.gate.outputs.exit-code }}
|
|
57
|
+
run: |
|
|
58
|
+
test "$OUTCOME" = "failure"
|
|
59
|
+
test "$EXIT_CODE" = "1"
|
|
60
|
+
|
|
61
|
+
- name: Clean demo (must pass with no results)
|
|
62
|
+
uses: ./
|
|
63
|
+
with:
|
|
64
|
+
path: examples/clean-repo
|
|
65
|
+
upload-sarif: "false"
|
|
66
|
+
|
|
67
|
+
- name: SARIF must be empty
|
|
68
|
+
run: |
|
|
69
|
+
python - <<'PY'
|
|
70
|
+
import json
|
|
71
|
+
results = json.load(open("ghostrules.sarif"))["runs"][0]["results"]
|
|
72
|
+
assert results == [], results
|
|
73
|
+
PY
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
- id: ghostrules
|
|
2
|
+
name: ghostrules (hidden instructions in AI agent files)
|
|
3
|
+
description: >-
|
|
4
|
+
Scan the repository for hidden Unicode, malicious instructions and risky MCP/agent
|
|
5
|
+
configuration in AGENTS.md, CLAUDE.md, .cursorrules, skills, MCP configs and similar files.
|
|
6
|
+
entry: ghostrules scan
|
|
7
|
+
language: python
|
|
8
|
+
pass_filenames: false
|
|
9
|
+
minimum_pre_commit_version: "3.0.0"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-10-27
|
|
9
|
+
|
|
10
|
+
First release.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Scanner for the files AI coding agents read and obey (`AGENTS.md`, `CLAUDE.md`, `.cursorrules`,
|
|
14
|
+
Copilot instructions, skills, MCP configs, agent settings and more), with `--home` for
|
|
15
|
+
user-level configs and `--all-files` for hidden Unicode anywhere in a tree.
|
|
16
|
+
- 21 rules. Hidden content: Unicode tag characters (GR001), bidirectional controls (GR002),
|
|
17
|
+
zero-width characters (GR003), variation-selector steganography (GR004), hidden HTML comments
|
|
18
|
+
(GR005), mixed-script homoglyphs (GR006). Malicious instructions, English and Spanish: instruction
|
|
19
|
+
override (GR010), concealment (GR011), remote code execution (GR012), data exfiltration (GR013),
|
|
20
|
+
sensitive file access (GR014), encoded payloads (GR015), safety bypass (GR016). Configuration:
|
|
21
|
+
hardcoded secrets (GR101), unpinned packages (GR102), insecure transport (GR103), broad
|
|
22
|
+
filesystem scope (GR104), shell wrappers (GR105), dangerous Docker options (GR106),
|
|
23
|
+
over-permissive agent settings (GR107), auto-trusted MCP servers (GR108).
|
|
24
|
+
- Hidden text decoded by GR001, GR004 and GR015 is re-scanned by GR010-GR016 and reported with
|
|
25
|
+
one more level of severity.
|
|
26
|
+
- CLI: `scan` (the default command), `reveal`, `clean`, `rules`, `version`. Reports as a table
|
|
27
|
+
grouped by file with the decoded hidden text highlighted, JSON, SARIF 2.1.0 (validated against the
|
|
28
|
+
official schema) and Markdown. Exit codes: 0 clean, 1 findings at or above `--fail-on`, 2 error.
|
|
29
|
+
- Configuration through `.ghostrules.toml` or `[tool.ghostrules]`, and inline `ghostrules-ignore`
|
|
30
|
+
suppressions (never for GR001, GR002 or text found inside hidden characters).
|
|
31
|
+
- GitHub Action (SARIF upload to code scanning, job summary, `fail-on` gate), pre-commit hook, CI
|
|
32
|
+
with a smoke test and a self-scan, and a tag-triggered release workflow using PyPI Trusted
|
|
33
|
+
Publishing.
|
|
34
|
+
- Demo repositories `examples/poisoned-repo` (every rule fires) and `examples/clean-repo` (no
|
|
35
|
+
findings), and `demo/demo.tape` to record the README GIF.
|
|
36
|
+
|
|
37
|
+
### Security
|
|
38
|
+
- Read-only: nothing found in a scanned file is executed, imported, downloaded or sent anywhere,
|
|
39
|
+
and the tool makes no network requests. Symbolic links are not followed.
|
|
40
|
+
- Secrets are never printed: every finding, warning and error passes through one masking step
|
|
41
|
+
(`ghp_****…a1b2`), and a test fails if a secret appears in any report format.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# ghostrules — project memory for Claude Code
|
|
2
|
+
|
|
3
|
+
## What this project is
|
|
4
|
+
`ghostrules` is a Python CLI + GitHub Action that scans a repository (and optionally the user's
|
|
5
|
+
machine) for **hidden instructions and risky configuration in the files AI coding agents read and
|
|
6
|
+
obey**: `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, Copilot instructions, skills, MCP configs, etc.
|
|
7
|
+
|
|
8
|
+
Tagline: *"Find the instructions your AI agent sees but you don't."*
|
|
9
|
+
|
|
10
|
+
It is a **defensive** security tool. It only reads files and reports findings. It never executes
|
|
11
|
+
anything it finds, never makes network requests, and never phones home.
|
|
12
|
+
|
|
13
|
+
## Source-of-truth documents (read before coding)
|
|
14
|
+
- `docs/SPEC.md` — features, CLI, file targets, output formats, config. **The contract.**
|
|
15
|
+
- `docs/RULES.md` — every detection rule: ID, severity, logic, true/false positive examples.
|
|
16
|
+
- `docs/PLAN.md` — build order, phase by phase, with acceptance criteria.
|
|
17
|
+
|
|
18
|
+
If something in the code disagrees with these docs, the docs win unless the user says otherwise.
|
|
19
|
+
If a doc is ambiguous, pick the simplest reasonable option and note it in `docs/DECISIONS.md`.
|
|
20
|
+
|
|
21
|
+
## Tech stack
|
|
22
|
+
- Python **3.10+**, packaged with `pyproject.toml` (hatchling), src layout: `src/ghostrules/`.
|
|
23
|
+
- CLI: **typer**; terminal output: **rich**. TOML config: `tomllib` (3.11+) / `tomli` fallback.
|
|
24
|
+
- No other runtime dependencies unless clearly justified. **No network libraries at runtime.**
|
|
25
|
+
- Dev: `pytest`, `pytest-cov`, `ruff` (lint + format), `mypy` (lenient is fine).
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
```bash
|
|
29
|
+
pip install -e ".[dev]" # install for development
|
|
30
|
+
pytest -q # run tests
|
|
31
|
+
ruff check . && ruff format . # lint + format
|
|
32
|
+
ghostrules scan examples/poisoned-repo # manual smoke test (must find issues)
|
|
33
|
+
ghostrules scan examples/clean-repo # must report zero findings
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Code conventions
|
|
37
|
+
- Each rule is a small class/function in `src/ghostrules/rules/` registered in one registry.
|
|
38
|
+
Rules receive a parsed `ScanTarget` (path, kind, text, lines, parsed JSON/TOML if any) and
|
|
39
|
+
yield `Finding` objects. Rules must be pure: no I/O, no global state.
|
|
40
|
+
- `Finding` fields: `rule_id, severity, title, message, path, line, column, end_column, snippet,
|
|
41
|
+
decoded (optional str), fix_hint`.
|
|
42
|
+
- Severities: `critical > high > medium > low > info`.
|
|
43
|
+
- Snippets must render invisible characters visibly, e.g. `⟦U+200B ZERO WIDTH SPACE⟧`.
|
|
44
|
+
- **Secrets must always be masked** in every output format (`ghp_****…a1b2`).
|
|
45
|
+
- Type hints everywhere; small functions; docstrings on public functions.
|
|
46
|
+
- Every rule needs at least one positive and one negative test.
|
|
47
|
+
|
|
48
|
+
## Safety rules for this repo (important)
|
|
49
|
+
- Test fixtures and demo files must be **defanged**: use `attacker.example`, `example.com`,
|
|
50
|
+
`exfil.example`, fake tokens like `ghp_EXAMPLEEXAMPLEEXAMPLEEXAMPLE0000` — never real
|
|
51
|
+
domains, real webhooks or working payloads.
|
|
52
|
+
- Fixtures containing invisible characters are **generated by `scripts/make_fixtures.py`**,
|
|
53
|
+
not hand-typed, so they are reproducible and reviewable.
|
|
54
|
+
- Never add code that executes, downloads or evaluates scanned content.
|
|
55
|
+
|
|
56
|
+
## Workflow
|
|
57
|
+
- Work phase by phase following `docs/PLAN.md`. After each phase: run tests + ruff, then
|
|
58
|
+
stop and summarize what was done and what is next.
|
|
59
|
+
- Commit after each phase with a conventional commit message (`feat:`, `test:`, `docs:`…).
|
|
60
|
+
- Keep the README in English and keep it updated when CLI flags change.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Contributing to ghostrules
|
|
2
|
+
|
|
3
|
+
Thanks for helping. A noisy scanner gets uninstalled, so **precision matters as much as
|
|
4
|
+
coverage**: every change to a detection comes with a positive and a negative test.
|
|
5
|
+
|
|
6
|
+
## Set up
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
git clone https://github.com/BardLaudian/ghostrules && cd ghostrules
|
|
10
|
+
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
11
|
+
pip install -e ".[dev]"
|
|
12
|
+
|
|
13
|
+
pytest -q # tests
|
|
14
|
+
ruff check . && ruff format . # lint and format
|
|
15
|
+
mypy src # types (lenient is fine)
|
|
16
|
+
ghostrules scan examples/poisoned-repo # must find issues, exit code 1
|
|
17
|
+
ghostrules scan examples/clean-repo # must report nothing, exit code 0
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The documents in `docs/` are the contract: `SPEC.md` (features), `RULES.md` (every rule) and
|
|
21
|
+
`DECISIONS.md` (choices made where they were ambiguous). If code and docs disagree, fix one of
|
|
22
|
+
them in the same pull request.
|
|
23
|
+
|
|
24
|
+
## Add a rule in three steps
|
|
25
|
+
|
|
26
|
+
1. **Write the rule.** Add a function to the module that fits (`src/ghostrules/rules/unicode.py`,
|
|
27
|
+
`injection.py`, `mcp.py` or `agent_settings.py`) and register it with the decorator:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
@rule(
|
|
31
|
+
"GR017", # next free id: GR0xx = instruction files, GR1xx = config
|
|
32
|
+
Severity.HIGH, # typical severity
|
|
33
|
+
"Short title",
|
|
34
|
+
"One sentence on what it finds and why it matters.",
|
|
35
|
+
("instructions",), # file kinds it applies to
|
|
36
|
+
scans_decoded=True, # also run on hidden text decoded by other rules?
|
|
37
|
+
)
|
|
38
|
+
def gr017_something(target: ScanTarget) -> Iterator[Finding]:
|
|
39
|
+
...
|
|
40
|
+
yield build_finding(target=target, rule_id="GR017", ...)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Rules are **pure**: they receive a `ScanTarget` and yield `Finding`s, with no I/O and no global
|
|
44
|
+
state. Use `build_finding` so line, column, snippet and secret masking come out right. Put
|
|
45
|
+
shared regular expressions in `rules/patterns.py`.
|
|
46
|
+
|
|
47
|
+
2. **Test it.** Add at least one positive and one negative test (see `tests/test_rules_*.py`). Add
|
|
48
|
+
a case to `scripts/make_demo.py` so `examples/poisoned-repo` triggers the rule (a test fails
|
|
49
|
+
if any rule does not fire there) and make sure `examples/clean-repo` still reports nothing.
|
|
50
|
+
Fixtures with invisible characters are generated by `scripts/make_fixtures.py`.
|
|
51
|
+
|
|
52
|
+
3. **Document it.** Describe the rule in `docs/RULES.md` (ID, severity, logic, true and false
|
|
53
|
+
positive examples), note any judgement call in `docs/DECISIONS.md`, add a line to
|
|
54
|
+
`CHANGELOG.md`, then refresh the README table:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python scripts/make_demo.py
|
|
58
|
+
python scripts/gen_rules_table.py --write
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Rules of the repository
|
|
62
|
+
|
|
63
|
+
- **Defanged only.** Fixtures, demos and tests use `attacker.example`, `exfil.example`,
|
|
64
|
+
`example.com` and obviously fake tokens such as `ghp_` + `EXAMPLE` repeated. Never real
|
|
65
|
+
domains, real webhooks or working payloads.
|
|
66
|
+
- **Invisible characters are generated, never typed.** Write them as escapes or with `chr()` in
|
|
67
|
+
the generators. A test fails if a literal invisible character ends up in the sources.
|
|
68
|
+
- **Never execute, download or evaluate scanned content**, and no network libraries at runtime.
|
|
69
|
+
- **Secrets are always masked.** Anything that prints text from a scanned file goes through
|
|
70
|
+
`ghostrules.secrets.mask_secrets` (findings do it for you).
|
|
71
|
+
- Type hints everywhere, small functions, docstrings on public functions, English README.
|
|
72
|
+
- Conventional commits (`feat:`, `fix:`, `test:`, `docs:`...).
|
|
73
|
+
|
|
74
|
+
## Pull request checklist
|
|
75
|
+
|
|
76
|
+
- [ ] `pytest -q`, `ruff check .`, `ruff format --check .` and `mypy src` pass
|
|
77
|
+
- [ ] positive and negative tests for every new or changed detection
|
|
78
|
+
- [ ] `examples/poisoned-repo` still triggers every rule; `examples/clean-repo` is still clean
|
|
79
|
+
- [ ] `docs/RULES.md`, `docs/DECISIONS.md`, `CHANGELOG.md` and the README table are updated
|
|
80
|
+
- [ ] nothing real in the fixtures: no secrets, domains or payloads that work
|
ghostrules-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pablo Seoane Temprano
|
|
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.
|