borborich/pgextassure

Portable pre-admission evidence for PostgreSQL extensions.

2

stars

52

commits

Python

primary language

Aug 22, 2026

updated

pgextassure.shbb.pro/
database
github-actions
postgresql
postgresql-extension
sbom
security
sigstore
static-analysis
supply-chain-security

README

PgExtAssure

Portable pre-admission security evidence for PostgreSQL extensions.

PgExtAssure inspects an extension source tree before a platform team allowlists, builds, or installs it. It reads PostgreSQL extension metadata and source files, reports security-relevant patterns, and can fail CI at a chosen severity.

It does not build, load, install, or execute the extension it scans.

PgExtAssure produces review evidence, not a certificate. A clean report is not proof that an extension is safe, and it is not legal, compliance, or security advice.

Canonical repository: https://github.com/borborich/pgextassure

Product site: https://pgextassure.shbb.pro/

The command-line executable and Python module are both named pgextassure.

Start without a meeting

Choose the shortest path for your role:

You areStart hereResult
Extension maintainerAdd the audit-only GitHub ActionA machine-readable report on every selected change
Platform or security reviewerCreate and verify Evidence Bundle 1.0Evidence bound to exact source, coverage, and policy inputs
Independent verifierRun the public reproduction workflowA public pass or failure record from infrastructure you control
Team with a real intake decisionRequest a bounded evaluationA customer-controlled admission evidence workflow and handoff report

No hosted PgExtAssure account is required. The static scanner does not intentionally send source, findings, or telemetry to a PgExtAssure service.

Public validation snapshot

PgExtAssure alpha.16 has been independently reproduced by multiple external software engineers. Three successful public workflow runs are recorded from independently controlled forks. The disclosure-safe public corpus index records 16 pinned projects and 2,114 files processed with alpha.15.

Read the exact run links, scope, and limitations in the Public validation record. These results demonstrate bounded reproducibility—not certification, customer adoption, production safety, or endorsement by an operator or employer.

For the problem and evidence-interface rationale, read PostgreSQL extension admission needs portable evidence.

The problem

Installing a PostgreSQL extension can cross unusually powerful trust boundaries. Installation and upgrade scripts may run with elevated database privileges; native modules may execute inside the PostgreSQL server process; and a trusted or allowlisted extension can expose those capabilities to users who are not superusers.

The admission decision is therefore difficult to review consistently:

  • source packages mix control metadata, install SQL, upgrade SQL, and native code;
  • reviewers need a repeatable inventory before they spend time on manual analysis;
  • conventional database linters inspect an existing database, after the extension has already crossed the admission boundary;
  • an allowlist says what may be installed, but does not explain why an entry is safe enough to allow.

PgExtAssure puts a deterministic, reviewable static gate before that decision.

Two-minute quickstart

From a clone of this repository:

git clone https://github.com/borborich/pgextassure.git
cd pgextassure
python -m venv .venv
source .venv/bin/activate
python -m pip install -e .

pgextassure scan /path/to/postgres-extension \
  --format text \
  --fail-on high

Equivalent module invocation:

python -m pgextassure scan /path/to/postgres-extension

Write a machine-readable report without blocking the current run:

pgextassure scan /path/to/postgres-extension \
  --format json \
  --output pgextassure.json \
  --fail-on none

For a compact review queue that preserves every source location while grouping only findings with a proven shared routine identity:

pgextassure scan /path/to/postgres-extension \
  --format grouped-json \
  --output pgextassure-grouped.json \
  --fail-on none

grouped-json is a separate report type. Its summary reports both raw finding count and root-cause count. Rules without an explicit semantic identity remain location-scoped and are never merged heuristically.

Create a deterministic work queue for a security engineer or AI coding agent:

pgextassure scan /path/to/postgres-extension \
  --format review-json \
  --output pgextassure-review.json \
  --fail-on none

Agent Review Pack 1.0 binds every task to the exact grouped report and source manifest, carries a closed disposition vocabulary, contains no source-file payloads, and explicitly cannot grant admission. See Agent Review Pack.

Create and verify the separate agent-authored Decision Ledger:

pgextassure review template pgextassure-review.json \
  --output pgextassure-decisions.json
pgextassure review verify \
  pgextassure-review.json \
  pgextassure-decisions.json

Verification requires exact task coverage and citations for every resolved disposition. A structurally valid ledger still has no admission authority.

If reviewed build metadata generates an install SQL or control file that is absent from the source tree, supply a pinned, non-executing generation plan:

pgextassure scan /path/to/postgres-extension \
  --generation-plan /path/to/generation-plan.json \
  --format grouped-json \
  --output pgextassure-grouped.json

PgExtAssure verifies every declared input SHA-256 and may apply bounded literal template substitutions in memory. It never runs the build. See Generation plans for the schema and trust boundary.

For a monorepository or source tree containing known generated aliases or oversized test fixtures, use a reviewed digest-bound scope plan:

pgextassure scan /path/to/postgres-extension \
  --scope-plan /path/to/scope-plan.json \
  --format grouped-json \
  --output pgextassure-grouped.json

Scope plans declare non-overlapping relative scan roots. Every excluded regular file is pinned to its exact bytes and every excluded symlink to its exact target text. Missing, changed, or unused exclusions fail closed. See Scope plans.

To introduce a gate into a repository with existing findings, create and review a root-cause baseline:

pgextassure baseline /path/to/postgres-extension \
  --created-on 2026-07-29 \
  --output pgextassure-baseline.json

pgextassure scan /path/to/postgres-extension \
  --baseline pgextassure-baseline.json \
  --format grouped-json \
  --fail-on high

New root causes still block. Baselined findings remain visible. Temporary exceptions require an exact root-cause ID, owner, reason, and expiry date; an expired exception blocks again. See Baselines and suppressions.

For a centrally reviewed gate, supply a strict organization policy:

pgextassure policy-template adoption \
  --output pgextassure-policy.json

pgextassure scan /path/to/postgres-extension \
  --policy pgextassure-policy.json \
  --format grouped-json \
  --output pgextassure-grouped.json

The policy owns the gate, can block exact capabilities or rules, and controls whether baseline/suppression mechanisms are allowed. See Organization policy.

Create a deterministic, independently verifiable pilot artifact:

pgextassure evidence create /path/to/postgres-extension \
  --policy pgextassure-policy.json \
  --created-on 2026-07-29 \
  --component-name example-extension \
  --output pgextassure-evidence.zip

pgextassure evidence verify pgextassure-evidence.zip

Evidence Bundle 1.0 binds the report, analyzed-source manifest, coverage, limited SPDX 2.3 inventory, and exact control-input bytes. It contains no source-file payloads. It can be signed with GitHub/Sigstore attestations or with an offline corporate RSA key:

pgextassure evidence sign pgextassure-evidence.zip \
  --private-key corporate-release-key.pem \
  --signer-id acme-security/postgresql-admission-key-01 \
  --statement-output pgextassure-signature.json \
  --signature-output pgextassure-signature.bin \
  --public-key-output pgextassure-public-key.pem

pgextassure evidence verify-signature pgextassure-evidence.zip \
  --statement pgextassure-signature.json \
  --signature pgextassure-signature.bin \
  --public-key pgextassure-public-key.pem \
  --expected-key-sha256 'sha256:TRUSTED_64_HEX_DIGEST'

See Evidence bundles, Corporate Evidence Signature Profile 1.0, and the enterprise pilot.

Evaluate signed evidence against an organization-owned trust policy and create a deterministic Admission Receipt:

pgextassure trust evaluate pgextassure-evidence.zip \
  --statement pgextassure-signature.json \
  --signature pgextassure-signature.bin \
  --public-key pgextassure-public-key.pem \
  --trust-policy enterprise-trust-policy.json \
  --evaluated-on 2026-07-29 \
  --request-id CHG-2026-0042 \
  --target postgresql-prod-eu/extension-slot-01 \
  --output pgextassure-admission-receipt.json

Enterprise Trust Policy 1.0 owns accepted signers, key validity/revocation, tool/ruleset/policy constraints, age limits, and receipt lifetime. A deny receipt remains independently verifiable. See Enterprise trust and Admission Receipts.

Create a deterministic buyer handoff after all evidence, trust, and verification records are staged:

pgextassure pilot package pilot-staging \
  --output pgextassure-enterprise-pilot.zip

pgextassure pilot verify-package pgextassure-enterprise-pilot.zip

The package command enforces a closed file set, release checksums, bounded content, flat non-symlink inputs, and private-key marker rejection. See Enterprise Pilot Package 1.0.

Validate both the package and a deployed TLS 1.3/mTLS Admission Gateway with one receiving-organization command:

pgextassure pilot accept pgextassure-enterprise-pilot.zip \
  --gateway-url https://pgextassure-gateway.example.internal \
  --ca-certificate gateway-ca.pem \
  --client-certificate pilot-client.pem \
  --client-key pilot-client-key.pem \
  --expected-package-sha256 sha256:PACKAGE_DIGEST \
  --expected-key-sha256 sha256:PUBLIC_KEY_FINGERPRINT \
  --expected-trust-policy-sha256 sha256:TRUST_POLICY_DIGEST \
  --expected-request-id CHG-2026-0042 \
  --expected-target postgresql-prod/extension-slot-01 \
  --expected-evaluated-on 2026-07-29 \
  --verified-on 2026-07-29 \
  --idempotency-key pilot-CHG-2026-0042-acceptance-01 \
  --output pgextassure-pilot-acceptance.json

The runner performs offline enforcement, positive and negative TLS checks, first admission, and byte-identical replay, then retains a canonical credential-free report. See Self-service pilot acceptance.

Exit behavior is controlled by --fail-on:

critical | high | medium | low | none

The process exits non-zero when at least one finding meets or exceeds the selected threshold. Scanner errors also exit non-zero.

Example finding

The exact rendering may evolve, but every format carries the same core evidence: rule identifier, severity, location, explanation, and remediation.

CRITICAL  sql.security-definer-search-path
      sql/example--1.0.sql:42

SECURITY DEFINER function does not establish a constrained search_path.
An attacker may be able to redirect an unqualified object reference.

Evidence:
  CREATE FUNCTION example.refresh_cache() ...
  SECURITY DEFINER;

Remediation:
  Set a safe search_path and schema-qualify referenced objects. Review the
  function against PostgreSQL's SECURITY DEFINER guidance.

Example summary:

PgExtAssure 0.1.0-alpha.17
Manifest: sha256:93c7a1aa82da96c290155124b31fcfaa15e369d105cef327c38c17e1b82d8128
Coverage: sha256:7cd80d20a4cdb7b1b88828e3d769f36a3353e6c955e07b65f717efa0d9c62a51 | Skipped: 0
Files: 6 | Findings: 0 (critical 0, high 0, medium 0, low 0)

Findings identify review work; they do not establish exploitability.

GitHub Action

The repository includes a composite Action that installs the local PgExtAssure package and scans the requested path. The target extension is never executed.

name: PgExtAssure

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  security-events: write

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - uses: borborich/pgextassure@v0.1.0-alpha.17
        with:
          path: .
          format: sarif
          output: pgextassure.sarif
          annotations: active
          max-annotations: "25"
          fail-on: high

      - name: Upload SARIF
        if: ${{ always() && github.event_name != 'pull_request' }}
        uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
        with:
          sarif_file: pgextassure.sarif

Action inputs:

InputDefaultMeaning
path.Extension source directory or supported input file
formatsariftext, json, grouped-json, review-json, or sarif
outputpgextassure.sarifReport file; set to an empty string for stdout
generation-planemptyOptional reviewed, pinned generated-artifact declaration
scope-planemptyOptional reviewed, digest-bound roots and exact exclusions
baselineemptyOptional reviewed root-cause baseline
suppressionsemptyOptional owner-attributed expiring suppressions
evaluated-oncurrent UTC dateExplicit YYYY-MM-DD suppression evaluation date
policyemptyOptional organization policy that owns the gate
annotationsnoneactive, all, or none root-cause annotations
max-annotations25Maximum annotation lines, from 2 through 50
evidence-outputemptyEnables evidence mode and writes Bundle 1.0
evidence-predicate-outputpgextassure-evidence-predicate.jsonVerified custom-attestation predicate
evidence-sbom-outputpgextassure-sbom.spdx.jsonVerified SPDX inventory
evidence-created-oncurrent UTC dateOptional explicit bundle date
component-namepostgresql-extensionNon-secret SPDX component name
component-versionemptyOptional SPDX component version
fail-onnoneMinimum blocking severity, or none
python-version3.11Python used to run PgExtAssure

Annotations are grouped by root cause, omit matched source evidence, and are bounded by max-annotations. active includes active and expired decisions; all also emits accepted baseline and suppression decisions as notices. A report output is required because workflow commands use stdout. See GitHub annotations.

When evidence-output is set, the Action creates and verifies a bundle instead of a standalone report. GitHub annotations are disabled in this mode. The Action exposes the bundle, predicate, and SPDX paths as evidence-bundle, evidence-predicate, and evidence-sbom outputs. Signing still requires an explicit caller-owned actions/attest step and OIDC permissions.

Pin a released commit SHA in higher-assurance workflows. Uploading SARIF sends the generated report to GitHub; review your repository visibility, retention, and access settings before enabling that step. The example uploads only on non-PR events because fork pull requests receive a read-only token. SARIF upload is available for public repositories and for eligible private/internal repositories with GitHub Code Security enabled.

SARIF artifact URIs are made relative to GITHUB_WORKSPACE when the target is inside that workspace, and special path characters are URI-encoded. The embedded actions/setup-python v7 runtime requires GitHub Actions Runner 2.327.1 or newer; GitHub-hosted runners already satisfy this requirement.

The Action is audit-only by default because some high-severity records are privileged-capability inventory rather than proven defects. Set fail-on explicitly only after reviewing a baseline and choosing a policy appropriate to the target platform.

Supported inputs

The static MVP scans local files only. It is intended for an unpacked PostgreSQL extension source tree containing some or all of:

  • primary and version-specific *.control files;
  • generated control templates (*.control.in);
  • matching install and upgrade *.sql / *.sql.in artifacts, including their version graph;
  • C/C header source (*.c, *.h);
  • Rust source (*.rs) and Cargo.toml.

It does not fetch a repository, download a release, unpack an untrusted archive, connect to PostgreSQL, or resolve package dependencies. Scan an already checked out, bounded directory. Directory scans fail closed when no supported source is found, when a supported source is non-regular, or when the tree contains a symlinked directory/source that could escape the reviewed boundary. It also enforces limits on total entries, directories, path depth, path length, source files, bytes, and findings.

Every report exposes the exact static boundary: analyzed files are content-hashed in the manifest, while unsupported entries appear in a bounded metadata-only skipped-file inventory. See Report schemas and coverage.

See Rule reference for the implemented checks and input coverage.

Normalize pgspot evidence

PgExtAssure can normalize saved pgspot 0.9.2 single-file output without executing pgspot or extension code:

pgextassure adapter pgspot sql/extension.sql \
  --stdout pgspot.stdout \
  --subject-path sql/extension.sql \
  --analyzer-version 0.9.2 \
  --exit-code 1 \
  --output pgspot.external-analysis.json

pgextassure adapter verify pgspot.external-analysis.json \
  --source sql/extension.sql \
  --stdout pgspot.stdout

The strict document binds the exact source and stdout bytes, declared analyzer version, saved exit code, counters, and normalized diagnostics. It is observational review evidence and does not grant or deny admission. See External analyzer evidence.

What the MVP looks for

Rules are grouped around pre-admission questions:

  • does control metadata declare privileged installation, sensitive requirements, or recursively included configuration?
  • do install or upgrade scripts contain COPY ... PROGRAM, server-side file access, untrusted procedural languages, or public execution grants?
  • are privileged functions missing defensive configuration such as a constrained search_path?
  • does C or Rust source indicate filesystem, process, network, background worker, or unsafe-code capability that needs manual review?
  • can the install/upgrade graph reach the declared default version, and can each artifact be associated with one unambiguous control-file scope?

PgExtAssure favors evidence and remediation over a single opaque score. A finding can be a true security defect, a deliberate privileged capability, or a false positive requiring suppression in a future release.

Honest limitations

The static MVP:

  • does not execute SQL or native code;
  • does not prove the absence of malicious or vulnerable behavior;
  • does not fully model PL/pgSQL, dynamic SQL, C/Rust semantics, PostgreSQL planner behavior, or runtime configuration;
  • cannot establish that a scanned source tree matches a distributed binary;
  • does not inspect transitive build dependencies or compiler behavior;
  • does not validate install/upgrade equivalence;
  • does not replace provenance verification, signature checking, sandboxed dynamic analysis, manual review, or production controls;
  • may report false positives and false negatives.

To keep hostile inputs from creating unbounded reports, a file emits at most 32 findings per rule; the first retained finding records when additional matches were omitted. A scan fails rather than returning a partial report if it exceeds the global finding limit.

Do not use a clean PgExtAssure result as the sole reason to grant superuser, filesystem, process, network, or preload privileges.

How PgExtAssure fits with other tools

These tools solve different problems and can be used alongside PgExtAssure.

  • pgspot is the mature, AST-based specialist for PostgreSQL SQL and PL/pgSQL extension security. PgExtAssure's built-in SQL checks are a small offline baseline, not a claim to replace pgspot. A production assurance pipeline should ingest pgspot findings rather than duplicate its deeper SQL analysis.
  • pghostile creates adversarial database objects and exercises extension tests to expose privilege escalation. It is a valuable dynamic test and necessarily executes SQL in PostgreSQL. PgExtAssure's default scan remains non-executing and covers package metadata, native capability, update topology, evidence normalization, and CI policy.
  • CMU ExtAnalyzer statically inventories extension API usage and dynamically measures installation and cross-extension compatibility. PgExtAssure is scoped to security-admission evidence rather than ecosystem compatibility research.
  • Splinter runs SQL lints against a PostgreSQL/Supabase project's existing schema. PgExtAssure inspects an extension package before it is admitted or installed.
  • pgextwlist enforces a runtime allowlist and can install approved extensions with elevated privileges. PgExtAssure supplies repeatable evidence for deciding what should enter that allowlist; it does not enforce installation policy.
  • pg_validate_extupgrade installs and upgrades an extension in PostgreSQL, then compares the resulting objects. It validates upgrade-path equivalence, not source security, and it necessarily executes the extension paths it tests.

Threat model

PgExtAssure assumes the scanned tree may be malicious or compromised. The MVP keeps that tree on the data side of the boundary: it reads recognized files and does not invoke its build system, SQL, hooks, or binaries.

The principal protected assets are:

  • PostgreSQL superuser and database-owner authority;
  • database confidentiality, integrity, and availability;
  • the PostgreSQL server process and host;
  • the integrity of the extension admission decision and its evidence.

The main adversaries are a malicious extension author, a compromised upstream release, and an otherwise benign maintainer who introduced an unsafe construct. Runtime-only behavior, unknown parser evasions, transitive dependency attacks, and source-to-binary substitution remain outside the static MVP's guarantees.

Read the complete threat model before using PgExtAssure as a CI gate.

Roadmap: isolated dynamic assurance

The next assurance layer is intentionally separate from the static scanner:

  1. reproduce and attest the source-to-artifact build;
  2. create an ephemeral, unprivileged PostgreSQL environment with no production credentials, default-deny network access, resource limits, and a disposable filesystem;
  3. exercise install, upgrade, rollback, and representative calls;
  4. capture catalog diffs, filesystem/process/network attempts, crashes, and other behavioral evidence;
  5. destroy the environment and link the report to exact source and artifact digests.

Dynamic analysis will still be evidence, not certification. Details are in the roadmap.

Public corpus loop

PgExtAssure maintains a pinned public corpus for reproducibility and scanner quality. The disclosure-safe Extension Assurance Index publishes analysis coverage, provenance, and capability profiles for public revisions while omitting findings, severities, rule identifiers, evidence, paths, and source excerpts.

A completed index entry means only that the pinned source was processed. It is not a security rating, certification, endorsement, vulnerability report, or allowlist decision. Project-specific security observations remain behind the corpus publication gate until they have been reviewed and, where appropriate, coordinated with the upstream maintainer.

No corpus upload or telemetry exists in the static MVP. Any future contribution flow must be explicit opt-in, preview the exact payload, and support deletion.

Privacy

  • The scanner itself operates on local files and does not intentionally make network requests in the MVP.
  • PgExtAssure does not intentionally send source, findings, or telemetry to a hosted service. CI setup and package-installation steps may download Python or build tooling independently of the scan.
  • Reports can contain filenames, line numbers, and source excerpts. Treat them as potentially sensitive.
  • --output writes only to the path you choose.
  • Third-party CI systems, artifact upload steps, and SARIF services have their own data-handling policies; PgExtAssure cannot control them.

Project documentation

Development

python -m pip install -e .
python -m unittest discover -s tests -v

The CI workflow runs the same unit-test command.

License

Apache License 2.0. See LICENSE.

Contributors

borborich

51 commits

borborich/pgextassure

Portable pre-admission evidence for PostgreSQL extensions.

2

stars

52

commits

Python

primary language

Aug 22, 2026

updated

pgextassure.shbb.pro/
database
github-actions
postgresql
postgresql-extension
sbom
security
sigstore
static-analysis
supply-chain-security

README

PgExtAssure

Portable pre-admission security evidence for PostgreSQL extensions.

PgExtAssure inspects an extension source tree before a platform team allowlists, builds, or installs it. It reads PostgreSQL extension metadata and source files, reports security-relevant patterns, and can fail CI at a chosen severity.

It does not build, load, install, or execute the extension it scans.

PgExtAssure produces review evidence, not a certificate. A clean report is not proof that an extension is safe, and it is not legal, compliance, or security advice.

Canonical repository: https://github.com/borborich/pgextassure

Product site: https://pgextassure.shbb.pro/

The command-line executable and Python module are both named pgextassure.

Start without a meeting

Choose the shortest path for your role:

You areStart hereResult
Extension maintainerAdd the audit-only GitHub ActionA machine-readable report on every selected change
Platform or security reviewerCreate and verify Evidence Bundle 1.0Evidence bound to exact source, coverage, and policy inputs
Independent verifierRun the public reproduction workflowA public pass or failure record from infrastructure you control
Team with a real intake decisionRequest a bounded evaluationA customer-controlled admission evidence workflow and handoff report

No hosted PgExtAssure account is required. The static scanner does not intentionally send source, findings, or telemetry to a PgExtAssure service.

Public validation snapshot

PgExtAssure alpha.16 has been independently reproduced by multiple external software engineers. Three successful public workflow runs are recorded from independently controlled forks. The disclosure-safe public corpus index records 16 pinned projects and 2,114 files processed with alpha.15.

Read the exact run links, scope, and limitations in the Public validation record. These results demonstrate bounded reproducibility—not certification, customer adoption, production safety, or endorsement by an operator or employer.

For the problem and evidence-interface rationale, read PostgreSQL extension admission needs portable evidence.

The problem

Installing a PostgreSQL extension can cross unusually powerful trust boundaries. Installation and upgrade scripts may run with elevated database privileges; native modules may execute inside the PostgreSQL server process; and a trusted or allowlisted extension can expose those capabilities to users who are not superusers.

The admission decision is therefore difficult to review consistently:

  • source packages mix control metadata, install SQL, upgrade SQL, and native code;
  • reviewers need a repeatable inventory before they spend time on manual analysis;
  • conventional database linters inspect an existing database, after the extension has already crossed the admission boundary;
  • an allowlist says what may be installed, but does not explain why an entry is safe enough to allow.

PgExtAssure puts a deterministic, reviewable static gate before that decision.

Two-minute quickstart

From a clone of this repository:

git clone https://github.com/borborich/pgextassure.git
cd pgextassure
python -m venv .venv
source .venv/bin/activate
python -m pip install -e .

pgextassure scan /path/to/postgres-extension \
  --format text \
  --fail-on high

Equivalent module invocation:

python -m pgextassure scan /path/to/postgres-extension

Write a machine-readable report without blocking the current run:

pgextassure scan /path/to/postgres-extension \
  --format json \
  --output pgextassure.json \
  --fail-on none

For a compact review queue that preserves every source location while grouping only findings with a proven shared routine identity:

pgextassure scan /path/to/postgres-extension \
  --format grouped-json \
  --output pgextassure-grouped.json \
  --fail-on none

grouped-json is a separate report type. Its summary reports both raw finding count and root-cause count. Rules without an explicit semantic identity remain location-scoped and are never merged heuristically.

Create a deterministic work queue for a security engineer or AI coding agent:

pgextassure scan /path/to/postgres-extension \
  --format review-json \
  --output pgextassure-review.json \
  --fail-on none

Agent Review Pack 1.0 binds every task to the exact grouped report and source manifest, carries a closed disposition vocabulary, contains no source-file payloads, and explicitly cannot grant admission. See Agent Review Pack.

Create and verify the separate agent-authored Decision Ledger:

pgextassure review template pgextassure-review.json \
  --output pgextassure-decisions.json
pgextassure review verify \
  pgextassure-review.json \
  pgextassure-decisions.json

Verification requires exact task coverage and citations for every resolved disposition. A structurally valid ledger still has no admission authority.

If reviewed build metadata generates an install SQL or control file that is absent from the source tree, supply a pinned, non-executing generation plan:

pgextassure scan /path/to/postgres-extension \
  --generation-plan /path/to/generation-plan.json \
  --format grouped-json \
  --output pgextassure-grouped.json

PgExtAssure verifies every declared input SHA-256 and may apply bounded literal template substitutions in memory. It never runs the build. See Generation plans for the schema and trust boundary.

For a monorepository or source tree containing known generated aliases or oversized test fixtures, use a reviewed digest-bound scope plan:

pgextassure scan /path/to/postgres-extension \
  --scope-plan /path/to/scope-plan.json \
  --format grouped-json \
  --output pgextassure-grouped.json

Scope plans declare non-overlapping relative scan roots. Every excluded regular file is pinned to its exact bytes and every excluded symlink to its exact target text. Missing, changed, or unused exclusions fail closed. See Scope plans.

To introduce a gate into a repository with existing findings, create and review a root-cause baseline:

pgextassure baseline /path/to/postgres-extension \
  --created-on 2026-07-29 \
  --output pgextassure-baseline.json

pgextassure scan /path/to/postgres-extension \
  --baseline pgextassure-baseline.json \
  --format grouped-json \
  --fail-on high

New root causes still block. Baselined findings remain visible. Temporary exceptions require an exact root-cause ID, owner, reason, and expiry date; an expired exception blocks again. See Baselines and suppressions.

For a centrally reviewed gate, supply a strict organization policy:

pgextassure policy-template adoption \
  --output pgextassure-policy.json

pgextassure scan /path/to/postgres-extension \
  --policy pgextassure-policy.json \
  --format grouped-json \
  --output pgextassure-grouped.json

The policy owns the gate, can block exact capabilities or rules, and controls whether baseline/suppression mechanisms are allowed. See Organization policy.

Create a deterministic, independently verifiable pilot artifact:

pgextassure evidence create /path/to/postgres-extension \
  --policy pgextassure-policy.json \
  --created-on 2026-07-29 \
  --component-name example-extension \
  --output pgextassure-evidence.zip

pgextassure evidence verify pgextassure-evidence.zip

Evidence Bundle 1.0 binds the report, analyzed-source manifest, coverage, limited SPDX 2.3 inventory, and exact control-input bytes. It contains no source-file payloads. It can be signed with GitHub/Sigstore attestations or with an offline corporate RSA key:

pgextassure evidence sign pgextassure-evidence.zip \
  --private-key corporate-release-key.pem \
  --signer-id acme-security/postgresql-admission-key-01 \
  --statement-output pgextassure-signature.json \
  --signature-output pgextassure-signature.bin \
  --public-key-output pgextassure-public-key.pem

pgextassure evidence verify-signature pgextassure-evidence.zip \
  --statement pgextassure-signature.json \
  --signature pgextassure-signature.bin \
  --public-key pgextassure-public-key.pem \
  --expected-key-sha256 'sha256:TRUSTED_64_HEX_DIGEST'

See Evidence bundles, Corporate Evidence Signature Profile 1.0, and the enterprise pilot.

Evaluate signed evidence against an organization-owned trust policy and create a deterministic Admission Receipt:

pgextassure trust evaluate pgextassure-evidence.zip \
  --statement pgextassure-signature.json \
  --signature pgextassure-signature.bin \
  --public-key pgextassure-public-key.pem \
  --trust-policy enterprise-trust-policy.json \
  --evaluated-on 2026-07-29 \
  --request-id CHG-2026-0042 \
  --target postgresql-prod-eu/extension-slot-01 \
  --output pgextassure-admission-receipt.json

Enterprise Trust Policy 1.0 owns accepted signers, key validity/revocation, tool/ruleset/policy constraints, age limits, and receipt lifetime. A deny receipt remains independently verifiable. See Enterprise trust and Admission Receipts.

Create a deterministic buyer handoff after all evidence, trust, and verification records are staged:

pgextassure pilot package pilot-staging \
  --output pgextassure-enterprise-pilot.zip

pgextassure pilot verify-package pgextassure-enterprise-pilot.zip

The package command enforces a closed file set, release checksums, bounded content, flat non-symlink inputs, and private-key marker rejection. See Enterprise Pilot Package 1.0.

Validate both the package and a deployed TLS 1.3/mTLS Admission Gateway with one receiving-organization command:

pgextassure pilot accept pgextassure-enterprise-pilot.zip \
  --gateway-url https://pgextassure-gateway.example.internal \
  --ca-certificate gateway-ca.pem \
  --client-certificate pilot-client.pem \
  --client-key pilot-client-key.pem \
  --expected-package-sha256 sha256:PACKAGE_DIGEST \
  --expected-key-sha256 sha256:PUBLIC_KEY_FINGERPRINT \
  --expected-trust-policy-sha256 sha256:TRUST_POLICY_DIGEST \
  --expected-request-id CHG-2026-0042 \
  --expected-target postgresql-prod/extension-slot-01 \
  --expected-evaluated-on 2026-07-29 \
  --verified-on 2026-07-29 \
  --idempotency-key pilot-CHG-2026-0042-acceptance-01 \
  --output pgextassure-pilot-acceptance.json

The runner performs offline enforcement, positive and negative TLS checks, first admission, and byte-identical replay, then retains a canonical credential-free report. See Self-service pilot acceptance.

Exit behavior is controlled by --fail-on:

critical | high | medium | low | none

The process exits non-zero when at least one finding meets or exceeds the selected threshold. Scanner errors also exit non-zero.

Example finding

The exact rendering may evolve, but every format carries the same core evidence: rule identifier, severity, location, explanation, and remediation.

CRITICAL  sql.security-definer-search-path
      sql/example--1.0.sql:42

SECURITY DEFINER function does not establish a constrained search_path.
An attacker may be able to redirect an unqualified object reference.

Evidence:
  CREATE FUNCTION example.refresh_cache() ...
  SECURITY DEFINER;

Remediation:
  Set a safe search_path and schema-qualify referenced objects. Review the
  function against PostgreSQL's SECURITY DEFINER guidance.

Example summary:

PgExtAssure 0.1.0-alpha.17
Manifest: sha256:93c7a1aa82da96c290155124b31fcfaa15e369d105cef327c38c17e1b82d8128
Coverage: sha256:7cd80d20a4cdb7b1b88828e3d769f36a3353e6c955e07b65f717efa0d9c62a51 | Skipped: 0
Files: 6 | Findings: 0 (critical 0, high 0, medium 0, low 0)

Findings identify review work; they do not establish exploitability.

GitHub Action

The repository includes a composite Action that installs the local PgExtAssure package and scans the requested path. The target extension is never executed.

name: PgExtAssure

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  security-events: write

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

      - uses: borborich/pgextassure@v0.1.0-alpha.17
        with:
          path: .
          format: sarif
          output: pgextassure.sarif
          annotations: active
          max-annotations: "25"
          fail-on: high

      - name: Upload SARIF
        if: ${{ always() && github.event_name != 'pull_request' }}
        uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
        with:
          sarif_file: pgextassure.sarif

Action inputs:

InputDefaultMeaning
path.Extension source directory or supported input file
formatsariftext, json, grouped-json, review-json, or sarif
outputpgextassure.sarifReport file; set to an empty string for stdout
generation-planemptyOptional reviewed, pinned generated-artifact declaration
scope-planemptyOptional reviewed, digest-bound roots and exact exclusions
baselineemptyOptional reviewed root-cause baseline
suppressionsemptyOptional owner-attributed expiring suppressions
evaluated-oncurrent UTC dateExplicit YYYY-MM-DD suppression evaluation date
policyemptyOptional organization policy that owns the gate
annotationsnoneactive, all, or none root-cause annotations
max-annotations25Maximum annotation lines, from 2 through 50
evidence-outputemptyEnables evidence mode and writes Bundle 1.0
evidence-predicate-outputpgextassure-evidence-predicate.jsonVerified custom-attestation predicate
evidence-sbom-outputpgextassure-sbom.spdx.jsonVerified SPDX inventory
evidence-created-oncurrent UTC dateOptional explicit bundle date
component-namepostgresql-extensionNon-secret SPDX component name
component-versionemptyOptional SPDX component version
fail-onnoneMinimum blocking severity, or none
python-version3.11Python used to run PgExtAssure

Annotations are grouped by root cause, omit matched source evidence, and are bounded by max-annotations. active includes active and expired decisions; all also emits accepted baseline and suppression decisions as notices. A report output is required because workflow commands use stdout. See GitHub annotations.

When evidence-output is set, the Action creates and verifies a bundle instead of a standalone report. GitHub annotations are disabled in this mode. The Action exposes the bundle, predicate, and SPDX paths as evidence-bundle, evidence-predicate, and evidence-sbom outputs. Signing still requires an explicit caller-owned actions/attest step and OIDC permissions.

Pin a released commit SHA in higher-assurance workflows. Uploading SARIF sends the generated report to GitHub; review your repository visibility, retention, and access settings before enabling that step. The example uploads only on non-PR events because fork pull requests receive a read-only token. SARIF upload is available for public repositories and for eligible private/internal repositories with GitHub Code Security enabled.

SARIF artifact URIs are made relative to GITHUB_WORKSPACE when the target is inside that workspace, and special path characters are URI-encoded. The embedded actions/setup-python v7 runtime requires GitHub Actions Runner 2.327.1 or newer; GitHub-hosted runners already satisfy this requirement.

The Action is audit-only by default because some high-severity records are privileged-capability inventory rather than proven defects. Set fail-on explicitly only after reviewing a baseline and choosing a policy appropriate to the target platform.

Supported inputs

The static MVP scans local files only. It is intended for an unpacked PostgreSQL extension source tree containing some or all of:

  • primary and version-specific *.control files;
  • generated control templates (*.control.in);
  • matching install and upgrade *.sql / *.sql.in artifacts, including their version graph;
  • C/C header source (*.c, *.h);
  • Rust source (*.rs) and Cargo.toml.

It does not fetch a repository, download a release, unpack an untrusted archive, connect to PostgreSQL, or resolve package dependencies. Scan an already checked out, bounded directory. Directory scans fail closed when no supported source is found, when a supported source is non-regular, or when the tree contains a symlinked directory/source that could escape the reviewed boundary. It also enforces limits on total entries, directories, path depth, path length, source files, bytes, and findings.

Every report exposes the exact static boundary: analyzed files are content-hashed in the manifest, while unsupported entries appear in a bounded metadata-only skipped-file inventory. See Report schemas and coverage.

See Rule reference for the implemented checks and input coverage.

Normalize pgspot evidence

PgExtAssure can normalize saved pgspot 0.9.2 single-file output without executing pgspot or extension code:

pgextassure adapter pgspot sql/extension.sql \
  --stdout pgspot.stdout \
  --subject-path sql/extension.sql \
  --analyzer-version 0.9.2 \
  --exit-code 1 \
  --output pgspot.external-analysis.json

pgextassure adapter verify pgspot.external-analysis.json \
  --source sql/extension.sql \
  --stdout pgspot.stdout

The strict document binds the exact source and stdout bytes, declared analyzer version, saved exit code, counters, and normalized diagnostics. It is observational review evidence and does not grant or deny admission. See External analyzer evidence.

What the MVP looks for

Rules are grouped around pre-admission questions:

  • does control metadata declare privileged installation, sensitive requirements, or recursively included configuration?
  • do install or upgrade scripts contain COPY ... PROGRAM, server-side file access, untrusted procedural languages, or public execution grants?
  • are privileged functions missing defensive configuration such as a constrained search_path?
  • does C or Rust source indicate filesystem, process, network, background worker, or unsafe-code capability that needs manual review?
  • can the install/upgrade graph reach the declared default version, and can each artifact be associated with one unambiguous control-file scope?

PgExtAssure favors evidence and remediation over a single opaque score. A finding can be a true security defect, a deliberate privileged capability, or a false positive requiring suppression in a future release.

Honest limitations

The static MVP:

  • does not execute SQL or native code;
  • does not prove the absence of malicious or vulnerable behavior;
  • does not fully model PL/pgSQL, dynamic SQL, C/Rust semantics, PostgreSQL planner behavior, or runtime configuration;
  • cannot establish that a scanned source tree matches a distributed binary;
  • does not inspect transitive build dependencies or compiler behavior;
  • does not validate install/upgrade equivalence;
  • does not replace provenance verification, signature checking, sandboxed dynamic analysis, manual review, or production controls;
  • may report false positives and false negatives.

To keep hostile inputs from creating unbounded reports, a file emits at most 32 findings per rule; the first retained finding records when additional matches were omitted. A scan fails rather than returning a partial report if it exceeds the global finding limit.

Do not use a clean PgExtAssure result as the sole reason to grant superuser, filesystem, process, network, or preload privileges.

How PgExtAssure fits with other tools

These tools solve different problems and can be used alongside PgExtAssure.

  • pgspot is the mature, AST-based specialist for PostgreSQL SQL and PL/pgSQL extension security. PgExtAssure's built-in SQL checks are a small offline baseline, not a claim to replace pgspot. A production assurance pipeline should ingest pgspot findings rather than duplicate its deeper SQL analysis.
  • pghostile creates adversarial database objects and exercises extension tests to expose privilege escalation. It is a valuable dynamic test and necessarily executes SQL in PostgreSQL. PgExtAssure's default scan remains non-executing and covers package metadata, native capability, update topology, evidence normalization, and CI policy.
  • CMU ExtAnalyzer statically inventories extension API usage and dynamically measures installation and cross-extension compatibility. PgExtAssure is scoped to security-admission evidence rather than ecosystem compatibility research.
  • Splinter runs SQL lints against a PostgreSQL/Supabase project's existing schema. PgExtAssure inspects an extension package before it is admitted or installed.
  • pgextwlist enforces a runtime allowlist and can install approved extensions with elevated privileges. PgExtAssure supplies repeatable evidence for deciding what should enter that allowlist; it does not enforce installation policy.
  • pg_validate_extupgrade installs and upgrades an extension in PostgreSQL, then compares the resulting objects. It validates upgrade-path equivalence, not source security, and it necessarily executes the extension paths it tests.

Threat model

PgExtAssure assumes the scanned tree may be malicious or compromised. The MVP keeps that tree on the data side of the boundary: it reads recognized files and does not invoke its build system, SQL, hooks, or binaries.

The principal protected assets are:

  • PostgreSQL superuser and database-owner authority;
  • database confidentiality, integrity, and availability;
  • the PostgreSQL server process and host;
  • the integrity of the extension admission decision and its evidence.

The main adversaries are a malicious extension author, a compromised upstream release, and an otherwise benign maintainer who introduced an unsafe construct. Runtime-only behavior, unknown parser evasions, transitive dependency attacks, and source-to-binary substitution remain outside the static MVP's guarantees.

Read the complete threat model before using PgExtAssure as a CI gate.

Roadmap: isolated dynamic assurance

The next assurance layer is intentionally separate from the static scanner:

  1. reproduce and attest the source-to-artifact build;
  2. create an ephemeral, unprivileged PostgreSQL environment with no production credentials, default-deny network access, resource limits, and a disposable filesystem;
  3. exercise install, upgrade, rollback, and representative calls;
  4. capture catalog diffs, filesystem/process/network attempts, crashes, and other behavioral evidence;
  5. destroy the environment and link the report to exact source and artifact digests.

Dynamic analysis will still be evidence, not certification. Details are in the roadmap.

Public corpus loop

PgExtAssure maintains a pinned public corpus for reproducibility and scanner quality. The disclosure-safe Extension Assurance Index publishes analysis coverage, provenance, and capability profiles for public revisions while omitting findings, severities, rule identifiers, evidence, paths, and source excerpts.

A completed index entry means only that the pinned source was processed. It is not a security rating, certification, endorsement, vulnerability report, or allowlist decision. Project-specific security observations remain behind the corpus publication gate until they have been reviewed and, where appropriate, coordinated with the upstream maintainer.

No corpus upload or telemetry exists in the static MVP. Any future contribution flow must be explicit opt-in, preview the exact payload, and support deletion.

Privacy

  • The scanner itself operates on local files and does not intentionally make network requests in the MVP.
  • PgExtAssure does not intentionally send source, findings, or telemetry to a hosted service. CI setup and package-installation steps may download Python or build tooling independently of the scan.
  • Reports can contain filenames, line numbers, and source excerpts. Treat them as potentially sensitive.
  • --output writes only to the path you choose.
  • Third-party CI systems, artifact upload steps, and SARIF services have their own data-handling policies; PgExtAssure cannot control them.

Project documentation

Development

python -m pip install -e .
python -m unittest discover -s tests -v

The CI workflow runs the same unit-test command.

License

Apache License 2.0. See LICENSE.

Contributors

borborich

51 commits

Languages

Python

97.9%