diff --git a/.github/instructions/snyk_rules.instructions.md b/.github/instructions/snyk_rules.instructions.md new file mode 100644 index 0000000..eef0634 --- /dev/null +++ b/.github/instructions/snyk_rules.instructions.md @@ -0,0 +1,14 @@ +--- +alwaysApply: true +always_on: true +trigger: always_on +applyTo: "**" +description: Snyk Security At Inception +--- + +# Project security best practices + +- Always run snyk_code_scan tool for new first party code that is generated in a Snyk-supported language. +- If any security issues are found based on newly introduced or modified code or dependencies, attempt to fix the issues using the results context from Snyk. +- Rescan the code after fixing the issues to ensure that the issues were fixed and that there are no newly introduced issues. +- Repeat this process until no new issues are found. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6825ace --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + security: + name: Security audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + - name: Upgrade pip and install pip-audit + run: python -m pip install --upgrade pip pip-audit + - name: Run pip-audit + run: python -m pip_audit -r requirements.txt -f json + - name: Generate CycloneDX SBOM + run: | + python tools/generate_cyclonedx_sbom.py + - name: Upload SBOM artifacts + uses: actions/upload-artifact@v4 + with: + name: sbom + path: | + sbom/pip-sbom.json + sbom/cyclonedx-bom.json + + test: + name: Run tests + runs-on: ubuntu-latest + needs: security + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + - name: Install runtime deps for tests + run: python -m pip install --upgrade pip Pillow pytest + - name: Run pytest + run: pytest -q + + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + - name: Install linter + run: python -m pip install --upgrade pip flake8 + - name: Run flake8 (non-failing) + run: flake8 --config .flake8 . --exit-zero diff --git a/requirements-pinned.txt b/requirements-pinned.txt new file mode 100644 index 0000000..8b87b3e --- /dev/null +++ b/requirements-pinned.txt @@ -0,0 +1,37 @@ +boolean.py==5.0 +CacheControl==0.14.4 +certifi==2026.1.4 +charset-normalizer==3.4.4 +colorama==0.4.6 +cyclonedx-python-lib==11.6.0 +defusedxml==0.7.1 +filelock==3.21.2 +flake8==7.3.0 +idna==3.11 +iniconfig==2.3.0 +license-expression==30.4.4 +markdown-it-py==4.0.0 +mccabe==0.7.0 +mdurl==0.1.2 +msgpack==1.1.2 +packageurl-python==0.17.6 +packaging==26.0 +pillow==12.1.1 +pip-api==0.0.34 +pip-requirements-parser==32.0.1 +pip_audit==2.10.0 +platformdirs==4.7.1 +pluggy==1.6.0 +py-serializable==2.1.0 +pycodestyle==2.14.0 +pyflakes==3.4.0 +Pygments==2.19.2 +pyparsing==3.3.2 +pytest==9.0.2 +requests==2.32.5 +rich==14.3.2 +sortedcontainers==2.4.0 +tomli==2.4.0 +tomli_w==1.2.0 +typing_extensions==4.15.0 +urllib3==2.6.3 diff --git a/requirements.txt b/requirements.txt index df2f240..131d6c1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ RPi.GPIO==0.7.1 spidev==3.5 -Pillow==9.4.0 +Pillow>=10.3.0 numpy==2.1.3 rich==13.9.4 pandas==2.2.3 diff --git a/sbom/pip-sbom.json b/sbom/pip-sbom.json new file mode 100644 index 0000000..8a9752e --- /dev/null +++ b/sbom/pip-sbom.json @@ -0,0 +1 @@ +[{"name": "boolean.py", "version": "5.0"}, {"name": "CacheControl", "version": "0.14.4"}, {"name": "certifi", "version": "2026.1.4"}, {"name": "charset-normalizer", "version": "3.4.4"}, {"name": "colorama", "version": "0.4.6"}, {"name": "cyclonedx-python-lib", "version": "11.6.0"}, {"name": "defusedxml", "version": "0.7.1"}, {"name": "filelock", "version": "3.21.2"}, {"name": "flake8", "version": "7.3.0"}, {"name": "idna", "version": "3.11"}, {"name": "iniconfig", "version": "2.3.0"}, {"name": "license-expression", "version": "30.4.4"}, {"name": "markdown-it-py", "version": "4.0.0"}, {"name": "mccabe", "version": "0.7.0"}, {"name": "mdurl", "version": "0.1.2"}, {"name": "msgpack", "version": "1.1.2"}, {"name": "packageurl-python", "version": "0.17.6"}, {"name": "packaging", "version": "26.0"}, {"name": "pillow", "version": "12.1.1"}, {"name": "pip", "version": "26.0.1"}, {"name": "pip-api", "version": "0.0.34"}, {"name": "pip_audit", "version": "2.10.0"}, {"name": "pip-requirements-parser", "version": "32.0.1"}, {"name": "platformdirs", "version": "4.7.1"}, {"name": "pluggy", "version": "1.6.0"}, {"name": "py-serializable", "version": "2.1.0"}, {"name": "pycodestyle", "version": "2.14.0"}, {"name": "pyflakes", "version": "3.4.0"}, {"name": "Pygments", "version": "2.19.2"}, {"name": "pyparsing", "version": "3.3.2"}, {"name": "pytest", "version": "9.0.2"}, {"name": "requests", "version": "2.32.5"}, {"name": "rich", "version": "14.3.2"}, {"name": "sortedcontainers", "version": "2.4.0"}, {"name": "tomli", "version": "2.4.0"}, {"name": "tomli_w", "version": "1.2.0"}, {"name": "typing_extensions", "version": "4.15.0"}, {"name": "urllib3", "version": "2.6.3"}] diff --git a/tools/generate_cyclonedx_sbom.py b/tools/generate_cyclonedx_sbom.py new file mode 100644 index 0000000..09f7836 --- /dev/null +++ b/tools/generate_cyclonedx_sbom.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Generate a minimal CycloneDX SBOM (JSON) from pip list JSON (sbom/pip-sbom.json). + +Writes output to `sbom/cyclonedx-bom.json`. +""" +import json +import os +from datetime import datetime + + +def main(): + sbom_dir = os.path.join(os.path.dirname(__file__), '..', 'sbom') + pip_sbom_path = os.path.join(sbom_dir, 'pip-sbom.json') + out_path = os.path.join(sbom_dir, 'cyclonedx-bom.json') + + if not os.path.exists(pip_sbom_path): + print(f"Input SBOM not found: {pip_sbom_path}") + return 1 + + with open(pip_sbom_path, 'r', encoding='utf-8') as f: + pkgs = json.load(f) + + components = [] + for p in pkgs: + comp = { + "type": "library", + "name": p.get('name'), + "version": p.get('version') + } + components.append(comp) + + bom = { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": f"urn:uuid:{datetime.utcnow().isoformat()}", + "version": 1, + "metadata": { + "timestamp": datetime.utcnow().isoformat() + "Z", + "tools": [{"vendor": "local", "name": "generate_cyclonedx_sbom.py", "version": "1"}] + }, + "components": components, + } + + os.makedirs(sbom_dir, exist_ok=True) + with open(out_path, 'w', encoding='utf-8') as f: + json.dump(bom, f, indent=2, ensure_ascii=False) + + print(f"Wrote CycloneDX SBOM to {out_path}") + return 0 + + +if __name__ == '__main__': + raise SystemExit(main())