Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "develop"
schedule:
interval: "weekly"
open-pull-requests-limit: 5

- package-ecosystem: "pip"
directory: "/"
target-branch: "develop"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
19 changes: 17 additions & 2 deletions .github/workflows/org-security-failure-collector.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,37 @@ jobs:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }}
LOOKBACK_HOURS: ${{ github.event_name == 'workflow_dispatch' && inputs.lookback_hours || '48' }}
ORG_SECURITY_FAILURE_APP_ID: ${{ vars.ORG_SECURITY_FAILURE_APP_ID }}
ORG_SECURITY_FAILURE_APP_PRIVATE_KEY: ${{ secrets.ORG_SECURITY_FAILURE_APP_PRIVATE_KEY }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Check GitHub App collector configuration
id: app-config
run: |
set -euo pipefail
if [[ -n "$ORG_SECURITY_FAILURE_APP_ID" && -n "$ORG_SECURITY_FAILURE_APP_PRIVATE_KEY" ]]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping org security failure collection because ORG_SECURITY_FAILURE_APP_ID or ORG_SECURITY_FAILURE_APP_PRIVATE_KEY is not configured."
fi

- name: Create org installation token
if: steps.app-config.outputs.configured == 'true'
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.ORG_SECURITY_FAILURE_APP_ID }}
private-key: ${{ secrets.ORG_SECURITY_FAILURE_APP_PRIVATE_KEY }}
app-id: ${{ env.ORG_SECURITY_FAILURE_APP_ID }}
private-key: ${{ env.ORG_SECURITY_FAILURE_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
permission-actions: read
permission-checks: read
permission-issues: write

- name: Collect security workflow failures
if: steps.app-config.outputs.configured == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
Expand Down
60 changes: 59 additions & 1 deletion .github/workflows/prepare-pypi-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,68 @@ jobs:
- name: Validate package build
run: |
set -euo pipefail
python -m pip install --disable-pip-version-check --no-cache-dir -r requirements-release.txt
python -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-release.txt
python -m pip_audit --local --vulnerability-service osv --progress-spinner off
python -m build
python -m twine check dist/*

- name: Generate release SBOM and provenance
run: |
set -euo pipefail
cyclonedx-py environment "$(python -c 'import sys; print(sys.executable)')" --output-reproducible --output-format JSON --output-file release-sbom.cdx.json
python - <<'PY'
import hashlib
import json
import os
import platform
import sys
from pathlib import Path

def file_record(path: Path) -> dict[str, object]:
return {
"path": str(path),
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
"bytes": path.stat().st_size,
}

dist = [file_record(path) for path in sorted(Path("dist").glob("*")) if path.is_file()]
if not dist:
raise SystemExit("no package distributions were built")

payload = {
"schema": "https://contextualwisdomlab.github.io/appguardrail/release-provenance/v1",
"repository": os.environ.get("GITHUB_REPOSITORY"),
"workflow": os.environ.get("GITHUB_WORKFLOW"),
"run_id": os.environ.get("GITHUB_RUN_ID"),
"run_attempt": os.environ.get("GITHUB_RUN_ATTEMPT"),
"ref": os.environ.get("GITHUB_REF"),
"sha": os.environ.get("GITHUB_SHA"),
"actor": os.environ.get("GITHUB_ACTOR"),
"python": {
"executable": sys.executable,
"version": sys.version,
"platform": platform.platform(),
},
"release_requirements": file_record(Path("requirements-release.txt")),
"dist": dist,
}
Path("release-provenance.json").write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
PY

- name: Upload release supply-chain evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-supply-chain-evidence
path: |
release-sbom.cdx.json
release-provenance.json
if-no-files-found: error
retention-days: 7

- name: Clean local release artifacts
run: |
rm -rf build dist *.egg-info release-sbom.cdx.json release-provenance.json

- name: Commit and push release branch
run: |
set -euo pipefail
Expand Down
59 changes: 58 additions & 1 deletion .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ jobs:

- name: Install build tooling
run: |
python -m pip install --disable-pip-version-check --no-cache-dir -r requirements-release.txt
python -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-release.txt

- name: Audit release build tooling
run: |
python -m pip_audit --local --vulnerability-service osv --progress-spinner off

- name: Build package distributions
run: |
Expand All @@ -35,6 +39,59 @@ jobs:
run: |
python -m twine check dist/*

- name: Generate release SBOM and provenance
run: |
set -euo pipefail
cyclonedx-py environment "$(python -c 'import sys; print(sys.executable)')" --output-reproducible --output-format JSON --output-file release-sbom.cdx.json
python - <<'PY'
import hashlib
import json
import os
import platform
import sys
from pathlib import Path

def file_record(path: Path) -> dict[str, object]:
return {
"path": str(path),
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
"bytes": path.stat().st_size,
}

dist = [file_record(path) for path in sorted(Path("dist").glob("*")) if path.is_file()]
if not dist:
raise SystemExit("no package distributions were built")

payload = {
"schema": "https://contextualwisdomlab.github.io/appguardrail/release-provenance/v1",
"repository": os.environ.get("GITHUB_REPOSITORY"),
"workflow": os.environ.get("GITHUB_WORKFLOW"),
"run_id": os.environ.get("GITHUB_RUN_ID"),
"run_attempt": os.environ.get("GITHUB_RUN_ATTEMPT"),
"ref": os.environ.get("GITHUB_REF"),
"sha": os.environ.get("GITHUB_SHA"),
"actor": os.environ.get("GITHUB_ACTOR"),
"python": {
"executable": sys.executable,
"version": sys.version,
"platform": platform.platform(),
},
"release_requirements": file_record(Path("requirements-release.txt")),
"dist": dist,
}
Path("release-provenance.json").write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
PY

- name: Upload release supply-chain evidence
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-supply-chain-evidence
path: |
release-sbom.cdx.json
release-provenance.json
if-no-files-found: error
retention-days: 7

- name: Upload package distributions
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
38 changes: 38 additions & 0 deletions .github/workflows/scorecard-analysis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Scorecard analysis

on:
push:
branches: ["develop"]
schedule:
- cron: "30 1 * * 6"

permissions: read-all

jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
contents: read
issues: read
pull-requests: read
checks: read
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Run analysis
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true

- name: Upload to code scanning
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
sarif_file: results.sarif
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@
## 2026-07-01 - O(N*M) Line Counting Optimization
**Learning:** In `scanner/cli/appguardrail.py`, the `_scan_file` loop calculates line numbers by calling `count_newlines("\n", 0, start_idx)` for *every* regex match. In files with many matches, this repeatedly scans the string from the beginning, resulting in O(N*M) performance (where N is file length and M is matches). This is a massive bottleneck.
**Action:** Since `re.finditer` yields matches strictly in order, always calculate line numbers progressively using a tracking variable `current_line` and `current_pos`. Update `current_line += count_newlines("\n", current_pos, start_idx)`. This makes the line calculation strictly O(N), bringing up to a 15x speedup for files with many hits.

## 2026-07-02 - Remove `re.search` fast-path pre-check
**Learning:** Python's `re.finditer` evaluates lazily by allocating a lightweight C-level `ScannerObject`. Using `re.search` as a fast-path pre-check before `re.finditer` is an anti-pattern that addresses a non-existent bottleneck and degrades performance for matched paths by evaluating the regex twice.
**Action:** Do not use `re.search` before `re.finditer` for optimization purposes.
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ appguardrail --help
Maintainers can prepare PyPI releases with GitHub Actions Bot and OpenCode
Agent. See [Release Automation](docs/release-automation.md).

For the productization roadmap, see the
[2B KRW sale readiness plan](docs/product/2026-07-02-2b-krw-sale-readiness-plan.md).

### Initialize security rules in your project

```bash
Expand Down Expand Up @@ -94,6 +97,9 @@ APPGUARDRAIL_TARGET_URL=https://your-authorized-test-host.example appguardrail s

# If CodeGraph is installed, prepare structural context for deeper review
appguardrail scan --codegraph .

# Save normalized findings for report generation or dashboard ingestion
appguardrail scan --findings-json reports/findings.json .
```

Detects:
Expand All @@ -115,6 +121,61 @@ documented rule fixtures until the lightweight engine grows structural matching.
Deploy-blocking counts focus on app code. Findings in docs, tests, examples,
and scanner fixtures stay visible but do not fail the deploy gate by default.

### Generate reports from findings

```bash
appguardrail report buyer-diligence \
--findings reports/findings.json \
--out reports/buyer-diligence.md \
--app-name "Demo SaaS" \
--repository "ContextualWisdomLab/demo"

appguardrail report founder-friendly \
--findings reports/findings.json \
--out reports/founder-security-review.md \
--app-name "Demo SaaS"

appguardrail report agency \
--findings reports/findings.json \
--out reports/agency-security-review.md \
--app-name "Demo SaaS" \
--client-name "Demo Client" \
--reviewer "Demo Agency"

appguardrail report fix-pack \
--findings reports/findings.json \
--out reports/fix-pack.md \
--based-on "pre-launch-review-001"
```

`appguardrail scan --findings-json` writes the normalized findings envelope that
the report command accepts. You can also pass a raw JSON array of findings or
any object with a `findings` array. Report types are:

- `buyer-diligence`: buyer-readable launch posture and evidence checklist.
- `founder-friendly`: plain-language summary for non-security founders.
- `agency`: client-ready technical review and retest notes.
- `fix-pack`: AI-ready remediation prompts and verification steps.

Reports omit raw secrets and expand normalized metadata into launch posture,
finding summaries, remediation, and verification checklists.

### Generate an organization buyer evidence bundle

```bash
appguardrail org-bundle
```

This writes `appguardrail-buyer-evidence/` with:

- `org-readiness.md`: buyer-readable organization readiness narrative.
- `buyer-evidence.json`: machine-readable KPI payload.
- `manifest.json`: source, timestamp, warning, repository, PR, and action bucket metadata.
- `README.md`: how to use the generated evidence packet.

Use `--owner`, `--bundle-dir`, `--repos-json`, or `--prs-json` only when you
need a non-default organization, custom artifact path, or offline snapshot.

### Install continuous monitoring

```bash
Expand Down
28 changes: 28 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Security Policy

## Reporting a Vulnerability

Please do not report unpatched vulnerabilities through public GitHub issues.

Preferred: use GitHub private vulnerability reporting for this repository:

- https://github.com/ContextualWisdomLab/appguardrail/security/advisories/new

If private reporting is unavailable, open a public issue that only asks for a secure disclosure channel. Do not include exploit details, secrets, personal data, or unreleased vulnerability information in a public issue.

When reporting, include:

- affected branch, tag, or commit
- reproduction steps
- impact assessment
- proof-of-concept input or sanitized logs when needed for safe reproduction

## Response Expectations

- acknowledgement target: within 7 days
- triage or status update target: within 30 days when a fix is feasible
- coordinated disclosure preferred after a fix or mitigation is available

## Safe Handling

Do not send production credentials, private keys, customer data, or copyrighted third-party source documents in reports. Use synthetic fixtures and sanitized evidence whenever possible.
Loading
Loading