kiff-scan 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.
- kiff_scan-0.1.0/.github/workflows/ci.yml +75 -0
- kiff_scan-0.1.0/.github/workflows/release.yml +142 -0
- kiff_scan-0.1.0/.github/workflows/verify-claims.yml +90 -0
- kiff_scan-0.1.0/.gitignore +15 -0
- kiff_scan-0.1.0/.kiff-scan.json +7 -0
- kiff_scan-0.1.0/CHANGELOG.md +49 -0
- kiff_scan-0.1.0/CONTRIBUTING.md +87 -0
- kiff_scan-0.1.0/LICENSE +21 -0
- kiff_scan-0.1.0/PKG-INFO +355 -0
- kiff_scan-0.1.0/README.md +307 -0
- kiff_scan-0.1.0/SECURITY.md +44 -0
- kiff_scan-0.1.0/action.yml +146 -0
- kiff_scan-0.1.0/docs/COVERAGE.md +161 -0
- kiff_scan-0.1.0/docs/RELEASING.md +96 -0
- kiff_scan-0.1.0/docs/THREAT_MODEL.md +92 -0
- kiff_scan-0.1.0/pyproject.toml +72 -0
- kiff_scan-0.1.0/src/kiff_scan/__init__.py +49 -0
- kiff_scan-0.1.0/src/kiff_scan/__main__.py +10 -0
- kiff_scan-0.1.0/src/kiff_scan/cli.py +211 -0
- kiff_scan-0.1.0/src/kiff_scan/config.py +117 -0
- kiff_scan-0.1.0/src/kiff_scan/detectors/__init__.py +10 -0
- kiff_scan-0.1.0/src/kiff_scan/detectors/decisions.py +234 -0
- kiff_scan-0.1.0/src/kiff_scan/detectors/reachability.py +146 -0
- kiff_scan-0.1.0/src/kiff_scan/detectors/sinks.py +271 -0
- kiff_scan-0.1.0/src/kiff_scan/engine.py +184 -0
- kiff_scan-0.1.0/src/kiff_scan/model.py +155 -0
- kiff_scan-0.1.0/src/kiff_scan/report/__init__.py +6 -0
- kiff_scan-0.1.0/src/kiff_scan/report/json_out.py +118 -0
- kiff_scan-0.1.0/src/kiff_scan/report/pretty.py +223 -0
- kiff_scan-0.1.0/src/kiff_scan/report/sarif.py +137 -0
- kiff_scan-0.1.0/src/kiff_scan/taxonomy.py +140 -0
- kiff_scan-0.1.0/tests/fixtures/hooked_agent.py +46 -0
- kiff_scan-0.1.0/tests/fixtures/mixed_governance.py +70 -0
- kiff_scan-0.1.0/tests/fixtures/ungoverned_ops.py +83 -0
- kiff_scan-0.1.0/tests/test_cli.py +166 -0
- kiff_scan-0.1.0/tests/test_engine.py +281 -0
- kiff_scan-0.1.0/tests/test_interprocedural.py +210 -0
- kiff_scan-0.1.0/tests/test_no_egress.py +180 -0
- kiff_scan-0.1.0/tests/test_zero_deps.py +83 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: "0 6 * * 1"
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
20
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: ${{ matrix.python-version }}
|
|
26
|
+
- run: python -m pip install --upgrade pip
|
|
27
|
+
- run: python -m pip install -e ".[dev]"
|
|
28
|
+
- run: python -m pytest -v
|
|
29
|
+
|
|
30
|
+
no-network:
|
|
31
|
+
name: test suite passes with no network access
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
- uses: actions/setup-python@v5
|
|
36
|
+
with:
|
|
37
|
+
python-version: "3.12"
|
|
38
|
+
- run: python -m pip install -e ".[dev]"
|
|
39
|
+
- name: Run the suite with no network access
|
|
40
|
+
# Isolate only the test process in a fresh network namespace with nothing
|
|
41
|
+
# but loopback. Dropping traffic globally (iptables -P OUTPUT DROP) would
|
|
42
|
+
# also sever the runner's own connection to GitHub and hang the job.
|
|
43
|
+
#
|
|
44
|
+
# The interpreter is resolved *before* sudo: sudo resets PATH, so
|
|
45
|
+
# resolving it inside would pick /usr/bin/python, which has no pytest.
|
|
46
|
+
run: |
|
|
47
|
+
set -euo pipefail
|
|
48
|
+
PY="$(command -v python)"
|
|
49
|
+
echo "using interpreter: $PY"
|
|
50
|
+
sudo unshare --net -- sh -c 'ip link set lo up; exec "$1" -m pytest -q' _ "$PY"
|
|
51
|
+
|
|
52
|
+
lint:
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
steps:
|
|
55
|
+
- uses: actions/checkout@v4
|
|
56
|
+
- uses: actions/setup-python@v5
|
|
57
|
+
with:
|
|
58
|
+
python-version: "3.12"
|
|
59
|
+
- run: python -m pip install ruff==0.6.9
|
|
60
|
+
- run: ruff check src tests
|
|
61
|
+
- run: ruff format --check src tests
|
|
62
|
+
|
|
63
|
+
self-scan:
|
|
64
|
+
name: kiff-scan scans itself
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
- uses: actions/setup-python@v5
|
|
69
|
+
with:
|
|
70
|
+
python-version: "3.12"
|
|
71
|
+
- run: python -m pip install -e .
|
|
72
|
+
# Scanning our own source must find nothing: kiff-scan exposes no agent
|
|
73
|
+
# tools. The fixtures are excluded by .kiff-scan.json because they contain
|
|
74
|
+
# deliberately unguarded code.
|
|
75
|
+
- run: kiff-scan scan . --fail-on low
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI via Trusted Publishing (OIDC). There is no API token stored
|
|
4
|
+
# in this repository: PyPI verifies a short-lived token minted by GitHub for this
|
|
5
|
+
# exact workflow file, in this exact repository and environment. Nothing to leak,
|
|
6
|
+
# nothing to rotate.
|
|
7
|
+
#
|
|
8
|
+
# One-time setup on PyPI, before the first release:
|
|
9
|
+
# PyPI -> Your projects (or "Publishing" for a new project) -> Add a pending
|
|
10
|
+
# publisher, with:
|
|
11
|
+
# Owner: kiff
|
|
12
|
+
# Repository: kiff-scan
|
|
13
|
+
# Workflow name: release.yml
|
|
14
|
+
# Environment: pypi
|
|
15
|
+
#
|
|
16
|
+
# Then push a tag to publish:
|
|
17
|
+
# git tag v0.1.0 && git push origin v0.1.0
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
push:
|
|
21
|
+
tags: ["v*"]
|
|
22
|
+
workflow_dispatch:
|
|
23
|
+
inputs:
|
|
24
|
+
target:
|
|
25
|
+
description: "Where to publish"
|
|
26
|
+
required: true
|
|
27
|
+
default: testpypi
|
|
28
|
+
type: choice
|
|
29
|
+
options: [testpypi, pypi]
|
|
30
|
+
|
|
31
|
+
permissions:
|
|
32
|
+
contents: read
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
# Never publish something that does not pass its own gates. The security
|
|
36
|
+
# claims in the README are enforced by these tests, so a release that skipped
|
|
37
|
+
# them would be a release of unverified claims.
|
|
38
|
+
verify:
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v4
|
|
42
|
+
- uses: actions/setup-python@v5
|
|
43
|
+
with:
|
|
44
|
+
python-version: "3.12"
|
|
45
|
+
- run: python -m pip install -e ".[dev]"
|
|
46
|
+
- name: Tests
|
|
47
|
+
run: python -m pytest -v
|
|
48
|
+
- name: Security gates
|
|
49
|
+
run: python -m pytest tests/test_no_egress.py tests/test_zero_deps.py -v
|
|
50
|
+
- name: Self-scan must be clean
|
|
51
|
+
run: kiff-scan scan . --fail-on low
|
|
52
|
+
|
|
53
|
+
build:
|
|
54
|
+
needs: verify
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
- uses: actions/setup-python@v5
|
|
59
|
+
with:
|
|
60
|
+
python-version: "3.12"
|
|
61
|
+
- run: python -m pip install build
|
|
62
|
+
- run: python -m build
|
|
63
|
+
- name: Tag must match the packaged version
|
|
64
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
65
|
+
run: |
|
|
66
|
+
set -euo pipefail
|
|
67
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
68
|
+
pkg="$(python -c 'import tomllib;print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')"
|
|
69
|
+
if [ "$tag" != "$pkg" ]; then
|
|
70
|
+
echo "tag v$tag does not match pyproject version $pkg"
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
echo "tag and version agree: $pkg"
|
|
74
|
+
- name: Wheel must declare no runtime dependencies
|
|
75
|
+
run: |
|
|
76
|
+
set -euo pipefail
|
|
77
|
+
python -m venv /tmp/clean
|
|
78
|
+
/tmp/clean/bin/python -m pip install --quiet dist/*.whl
|
|
79
|
+
extra=$(/tmp/clean/bin/python -m pip list --format=freeze \
|
|
80
|
+
| grep -v '^kiff-scan==' | grep -v '^pip==' \
|
|
81
|
+
| grep -v '^setuptools==' | grep -v '^wheel==' || true)
|
|
82
|
+
if [ -n "$extra" ]; then
|
|
83
|
+
echo "the wheel pulled in dependencies: $extra"
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
/tmp/clean/bin/kiff-scan --version
|
|
87
|
+
- uses: actions/upload-artifact@v4
|
|
88
|
+
with:
|
|
89
|
+
name: dist
|
|
90
|
+
path: dist/
|
|
91
|
+
|
|
92
|
+
publish-testpypi:
|
|
93
|
+
needs: build
|
|
94
|
+
if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
|
|
95
|
+
runs-on: ubuntu-latest
|
|
96
|
+
environment: testpypi
|
|
97
|
+
permissions:
|
|
98
|
+
id-token: write # required for Trusted Publishing
|
|
99
|
+
steps:
|
|
100
|
+
- uses: actions/download-artifact@v4
|
|
101
|
+
with:
|
|
102
|
+
name: dist
|
|
103
|
+
path: dist/
|
|
104
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
105
|
+
with:
|
|
106
|
+
repository-url: https://test.pypi.org/legacy/
|
|
107
|
+
|
|
108
|
+
publish-pypi:
|
|
109
|
+
needs: build
|
|
110
|
+
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
|
|
111
|
+
runs-on: ubuntu-latest
|
|
112
|
+
environment: pypi
|
|
113
|
+
permissions:
|
|
114
|
+
id-token: write # required for Trusted Publishing
|
|
115
|
+
steps:
|
|
116
|
+
- uses: actions/download-artifact@v4
|
|
117
|
+
with:
|
|
118
|
+
name: dist
|
|
119
|
+
path: dist/
|
|
120
|
+
# Publishes with PEP 740 provenance attestations by default, so a user can
|
|
121
|
+
# verify the artifact was built from this repository at this commit.
|
|
122
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
123
|
+
|
|
124
|
+
github-release:
|
|
125
|
+
needs: publish-pypi
|
|
126
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
permissions:
|
|
129
|
+
contents: write
|
|
130
|
+
steps:
|
|
131
|
+
- uses: actions/checkout@v4
|
|
132
|
+
- uses: actions/download-artifact@v4
|
|
133
|
+
with:
|
|
134
|
+
name: dist
|
|
135
|
+
path: dist/
|
|
136
|
+
- name: Create the GitHub release
|
|
137
|
+
env:
|
|
138
|
+
GH_TOKEN: ${{ github.token }}
|
|
139
|
+
run: |
|
|
140
|
+
gh release create "$GITHUB_REF_NAME" dist/* \
|
|
141
|
+
--title "$GITHUB_REF_NAME" \
|
|
142
|
+
--notes "See [CHANGELOG.md](./CHANGELOG.md) for what changed."
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: verify-claims
|
|
2
|
+
|
|
3
|
+
# Every factual claim the README makes about this package is checked here. If a
|
|
4
|
+
# claim stops being true, this workflow goes red before a human notices.
|
|
5
|
+
#
|
|
6
|
+
# The claims under test:
|
|
7
|
+
# 1. Zero runtime dependencies.
|
|
8
|
+
# 2. No network code anywhere in the package (no telemetry, no upload).
|
|
9
|
+
# 3. The scanner never executes the code it analyses.
|
|
10
|
+
# 4. Documented exit codes behave as documented.
|
|
11
|
+
# 5. The version in the README install line resolves.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
branches: [main]
|
|
16
|
+
pull_request:
|
|
17
|
+
schedule:
|
|
18
|
+
- cron: "0 6 * * *"
|
|
19
|
+
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
|
|
23
|
+
jobs:
|
|
24
|
+
verify:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v4
|
|
28
|
+
- uses: actions/setup-python@v5
|
|
29
|
+
with:
|
|
30
|
+
python-version: "3.12"
|
|
31
|
+
- run: python -m pip install -e ".[dev]"
|
|
32
|
+
|
|
33
|
+
- name: Claim - zero runtime dependencies
|
|
34
|
+
run: python -m pytest tests/test_zero_deps.py -v
|
|
35
|
+
|
|
36
|
+
- name: Claim - no egress, no telemetry, no code execution
|
|
37
|
+
run: python -m pytest tests/test_no_egress.py -v
|
|
38
|
+
|
|
39
|
+
- name: Claim - an installed wheel pulls in nothing
|
|
40
|
+
run: |
|
|
41
|
+
set -euo pipefail
|
|
42
|
+
# Build a real wheel. `pip download .` yields an sdist, not a wheel.
|
|
43
|
+
python -m pip install --quiet build
|
|
44
|
+
rm -rf /tmp/whl
|
|
45
|
+
python -m build --wheel --outdir /tmp/whl
|
|
46
|
+
python -m venv /tmp/clean
|
|
47
|
+
/tmp/clean/bin/python -m pip install --quiet /tmp/whl/*.whl
|
|
48
|
+
installed=$(/tmp/clean/bin/python -m pip list --format=freeze | grep -v '^kiff-scan==' | grep -v '^pip==' | grep -v '^setuptools==' | grep -v '^wheel==' || true)
|
|
49
|
+
if [ -n "$installed" ]; then
|
|
50
|
+
echo "installing kiff-scan pulled in: $installed"
|
|
51
|
+
exit 1
|
|
52
|
+
fi
|
|
53
|
+
/tmp/clean/bin/kiff-scan --version
|
|
54
|
+
echo "clean install contains only kiff-scan"
|
|
55
|
+
|
|
56
|
+
- name: Claim - documented exit codes
|
|
57
|
+
run: |
|
|
58
|
+
# errexit must be OFF here: these commands are *expected* to exit
|
|
59
|
+
# non-zero, and that exit code is the thing under test.
|
|
60
|
+
set +e
|
|
61
|
+
|
|
62
|
+
kiff-scan scan tests/fixtures/ungoverned_ops.py > /dev/null 2>&1
|
|
63
|
+
[ $? -eq 1 ] || { echo "expected exit 1 on findings"; exit 1; }
|
|
64
|
+
|
|
65
|
+
kiff-scan scan tests/fixtures/hooked_agent.py > /dev/null 2>&1
|
|
66
|
+
[ $? -eq 0 ] || { echo "expected exit 0 when all findings are governed"; exit 1; }
|
|
67
|
+
|
|
68
|
+
kiff-scan scan /nonexistent > /dev/null 2>&1
|
|
69
|
+
[ $? -eq 2 ] || { echo "expected exit 2 on usage error"; exit 1; }
|
|
70
|
+
|
|
71
|
+
kiff-scan scan tests/fixtures/ungoverned_ops.py --fail-on none > /dev/null 2>&1
|
|
72
|
+
[ $? -eq 0 ] || { echo "expected exit 0 with --fail-on none"; exit 1; }
|
|
73
|
+
|
|
74
|
+
echo "all documented exit codes behave as documented"
|
|
75
|
+
|
|
76
|
+
- name: Claim - SARIF output is valid JSON with the declared schema
|
|
77
|
+
run: |
|
|
78
|
+
set -euo pipefail
|
|
79
|
+
kiff-scan scan tests/fixtures --format sarif --output /tmp/out.sarif --fail-on none
|
|
80
|
+
python - <<'PY'
|
|
81
|
+
import json
|
|
82
|
+
with open("/tmp/out.sarif", encoding="utf-8") as fh:
|
|
83
|
+
doc = json.load(fh)
|
|
84
|
+
assert doc["version"] == "2.1.0", doc["version"]
|
|
85
|
+
run = doc["runs"][0]
|
|
86
|
+
declared = {r["id"] for r in run["tool"]["driver"]["rules"]}
|
|
87
|
+
for result in run["results"]:
|
|
88
|
+
assert result["ruleId"] in declared, result["ruleId"]
|
|
89
|
+
print(f"SARIF valid: {len(run['results'])} results, {len(declared)} rules")
|
|
90
|
+
PY
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "kiff-scan's own config. tests/fixtures/ contains deliberately unguarded sample code used as scanner input, so it is excluded from a scan of this repository. Without this, kiff-scan would report findings against its own test data.",
|
|
3
|
+
"exclude": [
|
|
4
|
+
"tests/fixtures/*",
|
|
5
|
+
"tests/fixtures/**"
|
|
6
|
+
]
|
|
7
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] — 2026-09-01
|
|
7
|
+
|
|
8
|
+
First release.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Static analysis of Python source for agent-reachable consequential actions.
|
|
13
|
+
A finding requires all three of: reachable by a model, consequential, and no
|
|
14
|
+
decision found on the analysed path.
|
|
15
|
+
- Eight consequence categories, seven of them marked `state_dependent` — the
|
|
16
|
+
cases where an authorization check is necessary but not sufficient because the
|
|
17
|
+
action's safety depends on live state at the moment of execution.
|
|
18
|
+
- Per-function decision detection with named evidence levels (`decorator`,
|
|
19
|
+
`call_before_sink`, `module_hook`, `call_after_sink`, `none`), including
|
|
20
|
+
lexical precedence so a guard that runs *after* the consequential call is
|
|
21
|
+
reported rather than credited.
|
|
22
|
+
- Vendor-neutral guard vocabulary: roughly 30 common authorization and approval
|
|
23
|
+
function names recognised out of the box, extensible with `--guard` or config.
|
|
24
|
+
- Match confidence (`call` vs `declared`), with name-only inferences capped below
|
|
25
|
+
`high` severity so they cannot fail a build on their own.
|
|
26
|
+
- Bounded interprocedural analysis: calls to functions defined in the same module
|
|
27
|
+
are followed up to four hops, so a tool that delegates its destructive work to
|
|
28
|
+
a helper is still reported, with the chain named in the reason. Cycles
|
|
29
|
+
terminate, and guards inside the chain are credited so the added reach does not
|
|
30
|
+
create false positives.
|
|
31
|
+
- Reporters: terminal, JSON (`schema_version: 1`), SARIF 2.1.0, and Markdown.
|
|
32
|
+
- `explain FILE:LINE` for the analysed path behind one finding.
|
|
33
|
+
- `--fail-on {none,low,medium,high}`, defaulting to `medium` in the CLI and
|
|
34
|
+
`none` in the GitHub Action.
|
|
35
|
+
- Unsupported files are reported explicitly and never counted as clean.
|
|
36
|
+
- Configuration via `.kiff-scan.json`: custom guards, custom tool decorators,
|
|
37
|
+
exclude globs, and excluded directories.
|
|
38
|
+
- GitHub Action with a sticky pull request comment and SARIF upload.
|
|
39
|
+
- Security gates in CI: no network-capable imports, no telemetry, no socket
|
|
40
|
+
opened during a scan, the suite passing with outbound traffic blocked, no
|
|
41
|
+
execution of analysed code, read-only on disk, and zero runtime dependencies
|
|
42
|
+
verified against a clean install of the built wheel.
|
|
43
|
+
|
|
44
|
+
### Notes
|
|
45
|
+
|
|
46
|
+
Python only in this release. TypeScript and Go files are reported as unsupported
|
|
47
|
+
rather than skipped silently. See [docs/COVERAGE.md](./docs/COVERAGE.md) for the
|
|
48
|
+
full list of what is and is not analysed, and the known limitations of decision
|
|
49
|
+
detection.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/kiff/kiff-scan
|
|
7
|
+
cd kiff-scan
|
|
8
|
+
python -m venv .venv && source .venv/bin/activate
|
|
9
|
+
pip install -e ".[dev]"
|
|
10
|
+
pytest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
There is no build step and no compilation. `pytest` should run in under a second.
|
|
14
|
+
|
|
15
|
+
## The two rules that are not negotiable
|
|
16
|
+
|
|
17
|
+
**1. No runtime dependencies.** `pyproject.toml`'s `dependencies` list stays
|
|
18
|
+
empty. This is the property that lets a security team run kiff-scan against a
|
|
19
|
+
codebase that has adopted nothing, and it is enforced by
|
|
20
|
+
`tests/test_zero_deps.py`. If you believe a dependency is genuinely required,
|
|
21
|
+
open an issue first — it is a product decision, not a refactor.
|
|
22
|
+
|
|
23
|
+
**2. No network code.** No `socket`, `urllib`, `http`, `requests`, or anything
|
|
24
|
+
that can open a connection, anywhere in the package. Also no `subprocess` and no
|
|
25
|
+
`ctypes`. Enforced by `tests/test_no_egress.py`, which checks the import graph,
|
|
26
|
+
runs a scan with the socket constructor sabotaged, and verifies a scan writes
|
|
27
|
+
nothing.
|
|
28
|
+
|
|
29
|
+
Both are checked on every push. A pull request that trips either gate will fail
|
|
30
|
+
before review.
|
|
31
|
+
|
|
32
|
+
## Adding a sink
|
|
33
|
+
|
|
34
|
+
Sinks live in `src/kiff_scan/detectors/sinks.py`.
|
|
35
|
+
|
|
36
|
+
1. Add the call's final attribute to `SINK_CALLS`, mapped to a consequence
|
|
37
|
+
category. Match the method name, not the full qualified path, so that
|
|
38
|
+
`client.delete_thing()` and `boto3.client("x").delete_thing()` both resolve.
|
|
39
|
+
2. Add a case to `tests/fixtures/` and assert it in `tests/test_engine.py`.
|
|
40
|
+
3. If it needs a new category, add it to `src/kiff_scan/taxonomy.py` and decide
|
|
41
|
+
`state_dependent` deliberately — see below.
|
|
42
|
+
|
|
43
|
+
## Adding a guard
|
|
44
|
+
|
|
45
|
+
Guards live in `src/kiff_scan/detectors/decisions.py`. Vendor neutrality is a
|
|
46
|
+
design commitment: a codebase's own `authorize()` must clear a finding exactly as
|
|
47
|
+
a KIFF decision does. Do not add a check that privileges KIFF over an equivalent
|
|
48
|
+
in-house guard.
|
|
49
|
+
|
|
50
|
+
## Deciding `state_dependent`
|
|
51
|
+
|
|
52
|
+
Ask: *can this action be fully authorized, requested by a legitimate principal,
|
|
53
|
+
with valid inputs, and still be the wrong thing to do because of the state of the
|
|
54
|
+
world right now?*
|
|
55
|
+
|
|
56
|
+
If yes, it is state-dependent (a rollback to a bad revision, a failover to the
|
|
57
|
+
active region, a second refund on an unsettled order). If an authorization check
|
|
58
|
+
plus input validation genuinely closes it, it is not (arbitrary shell execution).
|
|
59
|
+
|
|
60
|
+
Getting this wrong in either direction misleads the reader, so justify it in the
|
|
61
|
+
`why` field, which is printed in the report.
|
|
62
|
+
|
|
63
|
+
## Reporting a scanner mistake
|
|
64
|
+
|
|
65
|
+
A case where the scanner is wrong is the most valuable contribution. In scope:
|
|
66
|
+
|
|
67
|
+
- a supported-language sink it misses
|
|
68
|
+
- a recognised guard it fails to honour
|
|
69
|
+
- a finding on code with no model-controlled input on the path
|
|
70
|
+
- any case where it reports clean when it should not
|
|
71
|
+
|
|
72
|
+
Open an issue with a minimal reproducing file. Accepted cases become permanent
|
|
73
|
+
fixtures, credited by GitHub handle. Known misses stay listed even before they
|
|
74
|
+
are fixed; a challenge list that only shows fixed cases is a trophy cabinet.
|
|
75
|
+
|
|
76
|
+
## Style
|
|
77
|
+
|
|
78
|
+
`ruff check src tests` and `ruff format --check src tests` must pass. Comments
|
|
79
|
+
should explain *why*, especially where a detector is deliberately conservative —
|
|
80
|
+
that reasoning is the part a future reader cannot reconstruct.
|
|
81
|
+
|
|
82
|
+
## Tests
|
|
83
|
+
|
|
84
|
+
New behaviour needs a test. Fixtures under `tests/fixtures/` are scanner *input*
|
|
85
|
+
and must never be imported or collected; `pyproject.toml` ignores that directory
|
|
86
|
+
for collection, and the repository's own `.kiff-scan.json` excludes it from a
|
|
87
|
+
self-scan.
|
kiff_scan-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KIFF
|
|
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.
|