diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73de6ae..196f2f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,5 +95,7 @@ jobs: run: PYTHONPATH=graybox:whitebox:triager python3 -m unittest discover -s graybox/tests -p 'test_*.py' - name: Platform unit tests run: PYTHONPATH=platform:graybox:whitebox:triager:bundle python3 -m unittest discover -s platform/tests -p 'test_*.py' + - name: Docs site builds + run: pip install -r docs/requirements.txt && python docs/build.py - name: Reporter unit tests run: PYTHONPATH=reporter:triager python3 -m unittest discover -s reporter/tests -p 'test_*.py' diff --git a/.github/workflows/dev-push.yml b/.github/workflows/dev-push.yml index aade3f4..1cf3d10 100644 --- a/.github/workflows/dev-push.yml +++ b/.github/workflows/dev-push.yml @@ -1,4 +1,4 @@ -name: "Build and Push Dev/main OWASP OFFAT Docker Images to DockerHub" +name: "Build and push dev/main OWASP OFFAT image to Docker Hub" on: push: @@ -6,50 +6,26 @@ on: - "main" - "dev" +permissions: + contents: read + jobs: - build-and-push-dev-docker-images: + build-and-push: runs-on: ubuntu-latest steps: - - name: Branch Checkout - uses: actions/checkout@v2 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub - uses: docker/login-action@v3 + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Build and push offat-base docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/wolfi-base-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-base:${{ github.head_ref || github.ref_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/dev/cli-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:${{ github.head_ref || github.ref_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/dev/backend-api-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api:${{ github.head_ref || github.ref_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api-worker docker image - uses: docker/build-push-action@v3 + # One image: Rust execution engine + the Python platform (docker/Dockerfile). + - name: Build and push offat image + uses: docker/build-push-action@v6 with: - context: ./src/ - file: ./src/DockerFiles/dev/backend-api-worker-Dockerfile + context: . + file: ./docker/Dockerfile push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api-worker:${{ github.head_ref || github.ref_name }} + tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:${{ github.ref_name }} platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index d38f93c..03e47e0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,6 +1,6 @@ name: docs -# Build the documentation site (docs/, just-the-docs) and deploy it to +# Build the documentation site (docs/build.py -> docs/_site) and deploy it to # GitHub Pages. Requires Pages "Build and deployment" source = GitHub Actions. on: push: @@ -23,22 +23,20 @@ concurrency: jobs: build: runs-on: ubuntu-latest - defaults: - run: - working-directory: docs steps: - uses: actions/checkout@v4 - - uses: ruby/setup-ruby@v1 + - uses: actions/setup-python@v5 with: - ruby-version: "3.3" - bundler-cache: true - working-directory: docs + python-version: "3.12" - id: pages uses: actions/configure-pages@v5 - - name: Build with Jekyll - run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" + - name: Build the docs site + run: | + pip install -r docs/requirements.txt + python docs/build.py env: - JEKYLL_ENV: production + DOCS_BASE_PATH: ${{ steps.pages.outputs.base_path }}/ + DOCS_SITE_URL: ${{ steps.pages.outputs.base_url }}/ - uses: actions/upload-pages-artifact@v3 with: path: docs/_site diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml deleted file mode 100644 index 16d3210..0000000 --- a/.github/workflows/pypi-publish.yml +++ /dev/null @@ -1,43 +0,0 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries - -# This workflow uses actions that are not certified by GitHub. -# They are provided by a third-party and are governed by -# separate terms of service, privacy policy, and support -# documentation. - -name: Upload OWASP OFFAT Python Package to PyPi - -on: - release: - types: [published] - -permissions: - id-token: write # This is required for requesting the JWT - contents: read # This is required for actions/checkout - - -jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v3 - with: - python-version: "3.x" - - name: change cwd to src directory - run: cd src - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: | - cd src - python -m build - - name: Publish package - uses: pypa/gh-action-pypi-publish@v1.13.0 - with: - packages-dir: src/dist diff --git a/.github/workflows/release-push.yml b/.github/workflows/release-push.yml index c7bd369..9136776 100644 --- a/.github/workflows/release-push.yml +++ b/.github/workflows/release-push.yml @@ -1,88 +1,30 @@ -name: "Release: Build and Push OWASP OFFAT Docker Images to DockerHub" +name: "Release: build and push OWASP OFFAT image to Docker Hub" on: release: types: [published] +permissions: + contents: read + jobs: - build-and-push-main-docker-images: + build-and-push: runs-on: ubuntu-latest steps: - - name: Branch Checkout - uses: actions/checkout@v2 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - name: Login to Docker Hub - uses: docker/login-action@v2 + - uses: actions/checkout@v4 + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - # Build and publish version tag image - - name: Build and push offat-base docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/wolfi-base-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-base:${{ github.event.release.tag_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/cli-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:${{ github.event.release.tag_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/backend-api-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api:${{ github.event.release.tag_name }} - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api-worker docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/backend-api-worker-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api-worker:${{ github.event.release.tag_name }} - platforms: linux/amd64,linux/arm64 - - # Build and publish latest tag image - - name: Build and push offat-base docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/wolfi-base-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-base:latest - platforms: linux/amd64,linux/arm64 - - name: Build and push offat docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/cli-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat:latest - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api docker image - uses: docker/build-push-action@v3 - with: - context: ./src/ - file: ./src/DockerFiles/main/backend-api-Dockerfile - push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api:latest - platforms: linux/amd64,linux/arm64 - - name: Build and push offat-api-worker docker image - uses: docker/build-push-action@v3 + - name: Build and push offat image (version + latest) + uses: docker/build-push-action@v6 with: - context: ./src/ - file: ./src/DockerFiles/main/backend-api-worker-Dockerfile + context: . + file: ./docker/Dockerfile push: true - tags: ${{ secrets.DOCKERHUB_USERNAME }}/offat-api-worker:latest + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/offat:${{ github.event.release.tag_name }} + ${{ secrets.DOCKERHUB_USERNAME }}/offat:latest platforms: linux/amd64,linux/arm64 diff --git a/.gitignore b/.gitignore index 1acf502..c37ce5b 100644 --- a/.gitignore +++ b/.gitignore @@ -227,3 +227,6 @@ offat-report/ # OFFAT runtime artifacts .offat-triage-cache.json + +# docs site build output +docs/_site/ diff --git a/Makefile b/Makefile index 3da7694..41b7dd5 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,8 @@ engine-http3: ## Build the engine with the HTTP/3 + QUIC transport enabled engine-test: ## Run the engine's unit tests cd engine-rs && cargo test -platform-install: ## Install the Python platform + its libraries - pip install ./bundle ./triager ./whitebox ./graybox ./platform ./reporter +platform-install: ## Install the platform + its libraries (with the code-graph extra) + pip install ./bundle ./triager "./whitebox[graph]" ./graybox ./platform ./reporter python-check: ## Byte-compile the Python packages python3 -m compileall -q bundle/offat_bundle triager/offat_triage whitebox/offat_wb \ @@ -37,7 +37,7 @@ smoke: ## Offline end-to-end check (map -> prg -> threat-model -> test-gen) python3 -m offat_platform validate /tmp/offat-smoke.offat.json && echo "platform smoke OK" docker: ## Build the platform image (Rust engine + Python platform) - docker build -t offat-ai:latest -f docker/Dockerfile . + docker build -t offat:latest -f docker/Dockerfile . clean: ## Remove build artifacts and reports rm -rf engine-rs/target offat-report /tmp/offat-smoke*.json diff --git a/README.md b/README.md index 99df34c..1adacbe 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,86 @@ -# OWASP OFFAT +
+
+
+ OFFensive Api Tester — source-aware API security testing.
+ Maps your API from code, proves source→sink paths on a code graph, executes tests with a Rust engine,
+ and triages the results with AI.
+
+ Documentation · + Install · + Usage · + GitHub Action +
--- -OFFAT can be used in two ways. +> **Authorized use only.** Live runs send real requests to the target you point +> OFFAT at and require an explicit `--yes`. Only test systems and code you own or +> are authorized to assess. -## 1. Classic CLI - spec-driven black-box testing +## What it does -Generate and run tests from an OpenAPI/Swagger file or URL. - -```bash -python -m pip install offat # CLI only (offat[api] for CLI + REST API) -offat -f swagger_file.json # or: offat -f https://example.com/docs.json -offat -h # all options +``` +map -> code graph -> threat model -> test-gen -> execute (Rust) -> triage (AI) -> report ``` -Security checks: restricted HTTP methods, SQLi, BOLA, data exposure, BOPLA / -mass assignment, broken access control, basic command injection, and basic XSS / -HTML injection - a subset of the OWASP API Top 10. - -- Full usage: [owasp.github.io/OFFAT/usage](https://owasp.github.io/OFFAT/usage.html#classic-cli) -- CI: [GitHub Action](https://owasp.github.io/OFFAT/github-action.html) -- CLI details: [`src/README.md`](./src/README.md) +- **Map** the attack surface straight from source (decorator routes, routers, + OpenAPI/Swagger specs) into one canonical [Bundle](./bundle/README.md) file. +- **Prove** source-to-sink data flows on a code property graph (Python, JS/TS) and + attach every finding to the HTTP route that reaches it. +- **Execute** the test plan with a Rust engine over HTTP/1.1, HTTP/2, WebSocket and + gRPC (HTTP/3 + QUIC opt-in), with identity-aware differential checks for + BOLA / BFLA. +- **Triage** with a token-disciplined AI triager (cached, batched, model-tiered) + or a deterministic offline heuristic. +- **Report** as SARIF 2.1.0 (with code flows), Markdown, HTML, an OWASP API Top 10 + compliance report, and JUnit. -## 2. OFFAT-AI platform - source-aware testing +## Quick start -A staged pipeline that maps the attack surface from source (decorator routes -**and** OpenAPI specs), models threats, generates tests, executes them with a -Rust engine (HTTP/1.1 + HTTP/2, WebSocket, gRPC; HTTP/3 + QUIC opt-in), and -triages the findings - all over one canonical Bundle file. +```bash +git clone https://github.com/OWASP/OFFAT.git && cd OFFAT +pip install ./bundle ./triager './whitebox[graph]' ./graybox ./platform ./reporter +make engine # Rust engine, only for live runs -``` -map -> parameter relation graph -> threat model -> test-gen -> execute -> triage -> report -``` +# source-only: no traffic is sent +offat-platform run /path/to/repo -o app.offat.json --report-dir reports -```bash -# install the platform and its libraries -pip install ./bundle ./triager ./whitebox ./graybox ./platform ./reporter -# build the Rust execution engine (needed only for live runs) -make engine - -# the whole pipeline in one command (omit --url for a source-only run) -offat-platform run /path/to/repo \ - --url https://api.your-authorized-target.test \ - --identities examples/identities.example.json \ - --yes -o app.offat.json --report-dir reports --fail-on high +# live, against a target you are authorized to test +offat-platform run /path/to/repo --url https://api.your-authorized-target.test \ + --identities examples/identities.example.json --yes -o app.offat.json --fail-on high ``` -Two source-only modes send no live traffic: +Source-only modes: ```bash -offat-whitebox /path/to/repo --no-ai # SAST -offat-graybox /path/to/repo --fail-on high # SAST + endpoint reachability +offat-whitebox /path/to/repo # code-graph SAST + AI triage +offat-graybox /path/to/repo # SAST prioritized by endpoint reachability ``` -- Overview: [OFFAT-AI.md](./OFFAT-AI.md) -- How it works: [owasp.github.io/OFFAT/how-it-works](https://owasp.github.io/OFFAT/how-it-works.html) -- Source modes: [owasp.github.io/OFFAT/source-modes](https://owasp.github.io/OFFAT/source-modes.html) -- Design docs: [docs/platform.md](./docs/platform.md), [docs/architecture.md](./docs/architecture.md) +Or use the container: `docker build -t offat -f docker/Dockerfile .` + +## Repository layout -## Features +| Path | What | +|---|---| +| [`bundle/`](./bundle) | the Bundle contract (schema, IO, validation) | +| [`platform/`](./platform) | `offat-platform`: map, PRG, threat model, test-gen, triage | +| [`engine-rs/`](./engine-rs) | `offat-engine`: Rust execution engine | +| [`whitebox/`](./whitebox) | `offat-whitebox`: code-graph taint engine + SAST pipeline | +| [`graybox/`](./graybox) | `offat-graybox`: SAST fused with endpoint reachability | +| [`triager/`](./triager) | shared AI / heuristic triager | +| [`reporter/`](./reporter) | SARIF, Markdown, HTML, compliance reports | +| [`viz/`](./viz) | static Bundle visualizer | +| [`docs/`](./docs) | documentation site sources | -- Security checks from the OWASP API Top 10 -- Automated, spec-driven and source-aware testing -- User-config-based testing (YAML) -- REST API for automation and integrations -- CLI tools and a Dockerized project -- GitHub Action for CI -- Open source under the MIT License +> The classic spec-only Python CLI (`pip install offat`, `src/`) has been retired +> in favour of the platform. Its last release remains on PyPI. ## Contributing diff --git a/action.yml b/action.yml index 78f6a42..53638fb 100644 --- a/action.yml +++ b/action.yml @@ -1,56 +1,97 @@ -name: 'OWASP OFFAT CICD Scanner' -description: 'Autonomously assesses your API for prevalent vulnerabilities in CICD pipelines' +name: 'OWASP OFFAT' +description: 'Source-aware API security testing: map from code, prove source-to-sink flows, triage with AI, report as SARIF' branding: icon: shield color: blue inputs: - file: - description: 'path or url of openapi/swagger specification file' - required: true - - rate_limit: - description: 'API requests rate limit per second. default: 60' + path: + description: 'path to the source tree (and/or OpenAPI spec) to analyze' required: false - default: '60' - + default: '.' + url: + description: 'base URL of a target you are authorized to test. Empty = source-only run (no traffic sent)' + required: false + default: '' + authorized: + description: "must be 'true' for a live run against url - confirms you own or are authorized to test the target" + required: false + default: 'false' + identities: + description: 'optional identities JSON (enables BOLA / BFLA / RBAC / auth checks on live runs)' + required: false + default: '' + fail_on: + description: 'fail the job when an actionable finding is at or above this severity (critical|high|medium|low)' + required: false + default: '' + formats: + description: 'report formats (sarif,md,html,compliance,pdf or all)' + required: false + default: 'all' + upload_sarif: + description: "'true' to upload results.sarif to GitHub code scanning (needs security-events: write)" + required: false + default: 'false' artifact_retention_days: - description: 'artifact retention period in days. default: 2' + description: 'report artifact retention period in days' required: false - default: '2' + default: '7' runs: using: composite steps: - - name: Setup Python 3.12 - uses: actions/setup-python@v5.1.0 - with: - python-version: '3.12' - - - name: Install OFFAT - run: pip install -U offat - shell: bash - - - name: Run Scanner - env: - scanner_file: ${{ inputs.file }} - scanner_rate_limit: ${{ inputs.rate_limit }} + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' - run: | - mkdir -p ./offat-tmp/ - offat --file $scanner_file --rate-limit $scanner_rate_limit -of json -o ./offat-tmp/results.json --capture-failed -H 'User-Agent: github-action-offat' - shell: bash + - name: Install OFFAT + shell: bash + run: | + cd "$GITHUB_ACTION_PATH" + pip install ./bundle ./triager './whitebox[graph]' ./graybox ./platform ./reporter - - name: Upload Scan Results to Artifacts - uses: actions/upload-artifact@v4 - with: - name: offat-results.json - path: ./offat-tmp/results.json - retention-days: ${{ inputs.artifact_retention_days }} - # retention period docs: https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#artifact-and-log-retention-policy + - name: Build the execution engine (live runs only) + if: inputs.url != '' && inputs.authorized == 'true' + shell: bash + run: | + cargo build --release --manifest-path "$GITHUB_ACTION_PATH/engine-rs/Cargo.toml" + echo "OFFAT_ENGINE=$GITHUB_ACTION_PATH/engine-rs/target/release/offat-engine" >> "$GITHUB_ENV" - - name: CleanUp - run: rm -rf ./offat-tmp/ - shell: bash + - name: Run OFFAT + shell: bash + env: + OFFAT_PATH: ${{ inputs.path }} + OFFAT_URL: ${{ inputs.url }} + OFFAT_AUTHORIZED: ${{ inputs.authorized }} + OFFAT_IDENTITIES: ${{ inputs.identities }} + OFFAT_FAIL_ON: ${{ inputs.fail_on }} + OFFAT_FORMATS: ${{ inputs.formats }} + run: | + args=(run "$OFFAT_PATH" -o offat-out/app.offat.json --report-dir offat-out/reports --formats "$OFFAT_FORMATS") + if [ -n "$OFFAT_URL" ]; then + if [ "$OFFAT_AUTHORIZED" != "true" ]; then + echo "::error::url is set but authorized is not 'true'. Live runs send real traffic; only test targets you are authorized to assess." + exit 2 + fi + args+=(--url "$OFFAT_URL" --yes) + [ -n "$OFFAT_IDENTITIES" ] && args+=(--identities "$OFFAT_IDENTITIES") + fi + [ -n "$OFFAT_FAIL_ON" ] && args+=(--fail-on "$OFFAT_FAIL_ON") + offat-platform "${args[@]}" + - name: Upload reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: offat-reports + path: offat-out/ + retention-days: ${{ inputs.artifact_retention_days }} + - name: Upload SARIF to code scanning + if: always() && inputs.upload_sarif == 'true' && hashFiles('offat-out/reports/results.sarif') != '' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: offat-out/reports/results.sarif + category: offat diff --git a/docker/Dockerfile b/docker/Dockerfile index bc3817e..9106bf0 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -7,9 +7,9 @@ RUN cd engine-rs && cargo build --release && cp target/release/offat-engine /out # Stage 2 - runtime. FROM python:3.12-slim -LABEL org.opencontainers.image.title="offat-ai" \ +LABEL org.opencontainers.image.title="offat" \ org.opencontainers.image.description="AI-augmented API pentesting platform (map, threat-model, test-gen, execute, triage, report)" \ - org.opencontainers.image.source="https://github.com/dmdhrumilmistry/offat-ai" \ + org.opencontainers.image.source="https://github.com/OWASP/OFFAT" \ org.opencontainers.image.licenses="MIT" WORKDIR /app @@ -21,7 +21,7 @@ COPY graybox ./graybox COPY platform ./platform COPY reporter ./reporter COPY examples ./examples -RUN pip install --no-cache-dir ./bundle ./triager ./whitebox ./graybox ./platform ./reporter +RUN pip install --no-cache-dir ./bundle ./triager "./whitebox[graph]" ./graybox ./platform ./reporter # Non-root by default. RUN useradd -m offat diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index e9aefa5..0000000 --- a/docs/Gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source "https://rubygems.org" - -gem "jekyll", "~> 4.3.4" -gem "just-the-docs", "0.12.0" - -# Build/serve helpers used by newer Ruby and Jekyll 4. -gem "webrick", "~> 1.8" diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 0962234..0000000 --- a/docs/_config.yml +++ /dev/null @@ -1,57 +0,0 @@ -title: OWASP OFFAT -description: >- - OFFensive Api Tester - automated API security testing, from an OpenAPI spec - or straight from source. -theme: just-the-docs - -# Pages omit `layout` in their front matter, so apply the theme's layout to all -# of them - without this, Jekyll renders bare HTML with no CSS or navigation. -defaults: - - scope: - path: "" - values: - layout: default - -# Published at https://owasp.github.io/OFFAT/ via GitHub Actions (see -# .github/workflows/pages.yml). The workflow passes --baseurl, but set these -# so local `bundle exec jekyll serve` matches production. -url: https://owasp.github.io -baseurl: /OFFAT - -logo: /assets/offat.png -color_scheme: dark - -search_enabled: true -search: - heading_level: 2 - previews: 3 - button: true - -heading_anchors: true - -aux_links: - GitHub: - - https://github.com/OWASP/OFFAT - PyPI: - - https://pypi.org/project/offat/ -aux_links_new_tab: true - -nav_external_links: - - title: Report an issue - url: https://github.com/OWASP/OFFAT/issues - hide_icon: false - -footer_content: >- - OWASP OFFAT is distributed under the MIT License. Only test systems you own - or are explicitly authorized to assess. - -# These are design/internal docs with repo-relative links; they live in the -# repo for contributors but are not built as site pages. -exclude: - - Gemfile - - Gemfile.lock - - vendor - - architecture.md - - platform.md - - whitebox.md - - graybox.md diff --git a/docs/_theme/app.js b/docs/_theme/app.js new file mode 100644 index 0000000..2a0d9dd --- /dev/null +++ b/docs/_theme/app.js @@ -0,0 +1,240 @@ +/* OWASP OFFAT docs: theme, navigation drawer, search, code blocks, TOC spy. */ +(function () { + "use strict"; + var root = document.documentElement; + var body = document.body; + + /* ---------- theme ---------- */ + function effectiveTheme() { + var t = root.getAttribute("data-theme"); + if (t) return t; + return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + document.querySelectorAll("[data-toggle-theme]").forEach(function (btn) { + btn.addEventListener("click", function () { + var next = effectiveTheme() === "dark" ? "light" : "dark"; + root.setAttribute("data-theme", next); + try { localStorage.setItem("offat-theme", next); } catch (e) { /* storage unavailable */ } + }); + }); + + /* ---------- navigation drawer (mobile) ---------- */ + var scrim = document.querySelector(".scrim"); + function setNav(open) { + body.classList.toggle("nav-open", open); + if (scrim) scrim.hidden = !open; + } + document.querySelectorAll("[data-open-nav]").forEach(function (el) { + el.addEventListener("click", function () { setNav(true); }); + }); + document.querySelectorAll("[data-close-nav]").forEach(function (el) { + el.addEventListener("click", function () { setNav(false); }); + }); + + /* ---------- clipboard ---------- */ + function copyText(text, btn) { + function done() { + var old = btn.textContent; + btn.textContent = "copied"; + setTimeout(function () { btn.textContent = old; }, 1400); + } + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(done, function () {}); + } else { + var ta = document.createElement("textarea"); + ta.value = text; ta.style.position = "fixed"; ta.style.opacity = "0"; + document.body.appendChild(ta); ta.select(); + try { document.execCommand("copy"); done(); } catch (e) { /* ignore */ } + document.body.removeChild(ta); + } + } + document.querySelectorAll(".cmd").forEach(function (box) { + var btn = box.querySelector(".cmd-copy"); + if (btn) btn.addEventListener("click", function () { copyText(box.getAttribute("data-copy") || "", btn); }); + }); + + /* ---------- code blocks: header with language + copy ---------- */ + document.querySelectorAll(".prose pre").forEach(function (pre) { + var host = pre.parentElement && pre.parentElement.classList.contains("hl") ? pre.parentElement : pre; + if (host.parentElement && host.parentElement.classList.contains("codeblock")) return; + var code = pre.querySelector("code"); + var lang = host.getAttribute("data-lang") || ""; + if (!lang && code) { + var m = (code.className || "").match(/language-([\w+-]+)/); + if (m) lang = m[1]; + } + var wrap = document.createElement("div"); + wrap.className = "codeblock"; + var head = document.createElement("div"); + head.className = "code-head"; + var label = document.createElement("span"); + var dots = document.createElement("span"); + dots.className = "dots"; + for (var d = 0; d < 3; d++) dots.appendChild(document.createElement("i")); + label.appendChild(dots); + label.appendChild(document.createTextNode(lang || "text")); + head.appendChild(label); + var btn = document.createElement("button"); + btn.type = "button"; btn.className = "code-copy"; btn.textContent = "copy"; + btn.addEventListener("click", function () { copyText((code || pre).innerText.replace(/\n$/, ""), btn); }); + head.appendChild(btn); + host.parentNode.insertBefore(wrap, host); + wrap.appendChild(head); + wrap.appendChild(host); + }); + + /* ---------- table of contents scroll spy ---------- */ + var tocLinks = Array.prototype.slice.call(document.querySelectorAll(".toc a[href^='#']")); + if (tocLinks.length && "IntersectionObserver" in window) { + var byId = {}; + tocLinks.forEach(function (a) { byId[decodeURIComponent(a.getAttribute("href").slice(1))] = a; }); + var visible = {}; + var obs = new IntersectionObserver(function (entries) { + entries.forEach(function (e) { visible[e.target.id] = e.isIntersecting; }); + var heads = Object.keys(byId); + var current = null; + for (var i = 0; i < heads.length; i++) { if (visible[heads[i]]) { current = heads[i]; break; } } + if (current) { + tocLinks.forEach(function (a) { a.classList.remove("active"); }); + byId[current].classList.add("active"); + } + }, { rootMargin: "-70px 0px -65% 0px" }); + Object.keys(byId).forEach(function (id) { + var el = document.getElementById(id); + if (el) obs.observe(el); + }); + } + + /* ---------- hero pulse: align with the pipeline rail ---------- */ + function placePulse() { + var flow = document.querySelector(".flow"); + if (!flow) return; + var steps = flow.querySelectorAll(".flow-steps li"); + var pulse = flow.querySelector(".flow-pulse"); + if (!steps.length || !pulse) return; + var fr = flow.getBoundingClientRect(); + var first = steps[0].getBoundingClientRect(); + var last = steps[steps.length - 1].getBoundingClientRect(); + var top = first.top + first.height / 2 - fr.top - 3; + var end = last.top + last.height / 2 - fr.top - 3; + var rail = flow.querySelector(".flow-steps").getBoundingClientRect(); + pulse.style.top = top + "px"; + pulse.style.left = (rail.left - fr.left + 9) + "px"; + pulse.style.setProperty("--travel", (end - top) + "px"); + } + placePulse(); + window.addEventListener("resize", placePulse); + if (document.fonts && document.fonts.ready) document.fonts.ready.then(placePulse); + + /* ---------- search ---------- */ + var modal = document.getElementById("search"); + var input = document.getElementById("search-input"); + var list = document.getElementById("search-results"); + var index = null; + var sel = 0; + + function loadIndex() { + if (index) return Promise.resolve(index); + return fetch("search-index.json").then(function (r) { return r.json(); }).then(function (d) { + index = d; return d; + }).catch(function () { index = []; return index; }); + } + function esc(s) { + return String(s).replace(/[&<>"']/g, function (c) { + return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; + }); + } + function highlight(text, terms) { + var out = esc(text); + terms.forEach(function (t) { + if (!t) return; + var re = new RegExp("(" + t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "ig"); + out = out.replace(re, "$1"); + }); + return out; + } + function snippet(text, term) { + var i = text.toLowerCase().indexOf(term); + if (i < 0) return text.slice(0, 140); + var s = Math.max(0, i - 50); + return (s > 0 ? "… " : "") + text.slice(s, s + 150) + "…"; + } + function search(q) { + var terms = q.toLowerCase().split(/\s+/).filter(Boolean); + if (!terms.length) return []; + var results = []; + index.forEach(function (p) { + var title = p.t.toLowerCase(), text = (p.x || "").toLowerCase(), desc = (p.d || "").toLowerCase(); + var score = 0, all = true; + terms.forEach(function (t) { + var s = 0; + if (title.indexOf(t) >= 0) s += 12; + if (desc.indexOf(t) >= 0) s += 4; + if (text.indexOf(t) >= 0) s += 1 + Math.min(4, text.split(t).length - 1) * 0.5; + if (!s) all = false; + score += s; + }); + if (all && score) results.push({ url: p.u, title: p.t, meta: p.s, snip: snippet(p.x || p.d || "", terms[0]), score: score }); + (p.h || []).forEach(function (h) { + var hn = h.n.toLowerCase(); + if (terms.every(function (t) { return hn.indexOf(t) >= 0; })) { + results.push({ url: p.u + "#" + h.i, title: h.n, meta: p.t, snip: "", score: 10 + terms.length * 3 }); + } + }); + }); + results.sort(function (a, b) { return b.score - a.score; }); + return results.slice(0, 12).map(function (r) { r.terms = terms; return r; }); + } + function render(results, q) { + sel = 0; + if (!q) { list.innerHTML = 'OWASP · open source · MIT
+OFFAT maps your API from source, follows every input across files on a code graph, + runs its test plan with a Rust engine and triages the results with AI — or fully offline.
+offat-platform run ./repo -o app.offat.json
+
+ What you get
+Interprocedural data flow for Python and JS/TS. Findings carry the hop-by-hop path and the route that reaches them.
+ + + 02 +Every stage reads and writes one versioned JSON document. Stop, inspect, edit and resume anywhere.
+ + + 03 +Give it a few identities and it plans object- and function-level authorization checks with differential baselines.
+ + + 04 +Cached, batched and model-tiered triage that sees the proven path, plus an agent that reads the code around it.
+ + + 05 +A single static binary for the network-heavy part: HTTP/1.1 and HTTP/2, WebSocket, gRPC, HTTP/3 opt-in.
+ + + 06 +SARIF with code flows for GitHub code scanning, JUnit, Markdown, HTML and a --fail-on gate.
{inner}' + content = re.sub(r"
(.*?)", callout, content, flags=re.S) + # Tables scroll horizontally on narrow screens. + content = re.sub(r"
On this page
404
The trace went cold. Try the search, or start from the overview.
' + '