diff --git a/.github/codeql-allowlist.yml b/.github/codeql-allowlist.yml new file mode 100644 index 0000000..2d26ba8 --- /dev/null +++ b/.github/codeql-allowlist.yml @@ -0,0 +1,4 @@ +# CodeQL finding allowlist with expiry model. +# New entries require security on-call approval. +# Expired entries fail CI via scripts/check-codeql-allowlist.sh. +allowlist: [] diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..06a4e1a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,71 @@ +name: codeql + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 6 * * 1" + +permissions: + actions: read + contents: read + security-events: write + +jobs: + allowlist-expiry: + name: "IntentProof Security: CodeQL Allowlist" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Validate allowlist expiry dates + run: bash ./scripts/check-codeql-allowlist.sh + + gitleaks: + name: "IntentProof Security: Secret Scan" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install gitleaks + run: | + curl -sSfL \ + "https://github.com/gitleaks/gitleaks/releases/download/v8.24.2/gitleaks_8.24.2_linux_x64.tar.gz" \ + | tar -xz + sudo install -m 755 gitleaks /usr/local/bin/gitleaks + + - name: Run gitleaks + run: gitleaks detect --source . --config .gitleaks.toml --verbose --redact + + analyze: + name: "IntentProof Security: CodeQL (${{ matrix.language }})" + needs: allowlist-expiry + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language: [python] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install package for CodeQL + run: pip install -e ".[dev]" + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..a675bf7 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,21 @@ +# IntentProof gitleaks configuration. +# See https://github.com/gitleaks/gitleaks for rule documentation. + +title = "IntentProof gitleaks config" + +[extend] +useDefault = true + +[allowlist] +description = "Global allowlist paths for false positives" +regexTarget = "match" +paths = [ + '''\.git/''', + '''\.coverage''', + '''tests/fixtures/''', +] +# Example test vectors and documentation placeholders only. +regexes = [ + '''EXAMPLE|PLACEHOLDER|REDACTED|xxxxxxxx''', + '''_instance_private_key:\s*Ed25519PrivateKey''', +] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b8a004b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.24.2 + hooks: + - id: gitleaks diff --git a/scripts/check-codeql-allowlist.sh b/scripts/check-codeql-allowlist.sh new file mode 100755 index 0000000..4c9072a --- /dev/null +++ b/scripts/check-codeql-allowlist.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Validate .github/codeql-allowlist.yml expiry dates. +# Expired entries fail with a clear message for security on-call follow-up. +set -euo pipefail + +ALLOWLIST_FILE="${1:-.github/codeql-allowlist.yml}" + +if [[ ! -f "$ALLOWLIST_FILE" ]]; then + echo "No allowlist file at $ALLOWLIST_FILE; skipping expiry check." + exit 0 +fi + +if ! command -v python3 >/dev/null 2>&1; then + echo "python3 is required to validate $ALLOWLIST_FILE" >&2 + exit 1 +fi + +python3 - "$ALLOWLIST_FILE" <<'PY' +import datetime +import re +import sys + +path = sys.argv[1] +text = open(path, encoding="utf-8").read() + +# Minimal parser for our allowlist schema (no PyYAML dependency). +entries = [] +current = None +for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("- "): + if current: + entries.append(current) + current = {} + item = stripped[2:].strip() + m = re.search(r"rule_id:\s*(\S+)", item) + if m: + current["rule_id"] = m.group(1) + m = re.search(r"expires:\s*(\S+)", item) + if m: + current["expires"] = m.group(1) + continue + if current is None: + continue + m = re.match(r"expires:\s*(\S+)", stripped) + if m: + current["expires"] = m.group(1) + m = re.match(r"rule_id:\s*(\S+)", stripped) + if m: + current["rule_id"] = m.group(1) +if current: + entries.append(current) + +today = datetime.date.today() +expired = [] +for idx, entry in enumerate(entries): + expires_raw = entry.get("expires") + if not expires_raw: + print( + f"{path}: allowlist[{idx}] missing expires date " + "(required for security on-call approval model)", + file=sys.stderr, + ) + sys.exit(1) + try: + expires = datetime.date.fromisoformat(str(expires_raw)) + except ValueError: + print( + f"{path}: allowlist[{idx}] has invalid expires date: {expires_raw!r}", + file=sys.stderr, + ) + sys.exit(1) + if expires < today: + rule_id = entry.get("rule_id", "") + expired.append(f"{rule_id} (expired {expires.isoformat()})") + +if expired: + print("Allowlist expired; contact security on-call to extend or remove:", file=sys.stderr) + for item in expired: + print(f" - {item}", file=sys.stderr) + sys.exit(1) + +print(f"PASS: {len(entries)} allowlist entries are current.") +PY