Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
72a4192
fix: stabilize CI and isolate ExternalId verification
EdwinJdevops Sep 14, 2026
6f8e84f
feat: introduce provider-native drift evidence core
EdwinJdevops Sep 14, 2026
4cb068d
fix: enforce tenant ownership on scan retrieval
EdwinJdevops Sep 14, 2026
73dbb4c
test: prove evidence core against real Terraform and OpenTofu drift
EdwinJdevops Sep 14, 2026
1385e57
feat: add local redacted evidence analysis interface
EdwinJdevops Sep 14, 2026
5c31cac
fix: satisfy evidence CLI lint gate
EdwinJdevops Sep 14, 2026
99bd27d
fix: remove false precision from evidence paths
EdwinJdevops Sep 14, 2026
dea4c97
fix: satisfy reviewed evidence lint gate
EdwinJdevops Sep 14, 2026
829d868
feat: add evidence incident lifecycle and replay-safe identity
EdwinJdevops Sep 14, 2026
932c19c
fix: make evidence lifecycle monotonic under out-of-order scans
EdwinJdevops Sep 14, 2026
88c8861
fix: keep naive-time regression without weakening datetime lint
EdwinJdevops Sep 14, 2026
847c400
feat: establish fail-closed Alembic migration authority
EdwinJdevops Sep 17, 2026
6f20780
fix: satisfy migration revision lint without changing schema behavior
EdwinJdevops Sep 17, 2026
a781e95
feat: add redacted provider-native evidence ingestion
EdwinJdevops Sep 17, 2026
4e551c8
fix: satisfy evidence ingestion lint gate
EdwinJdevops Sep 18, 2026
b164a04
feat: make Alembic bootstrap the deployment schema gate
EdwinJdevops Sep 18, 2026
5a0764d
test: add disposable real AWS provider drift proof
EdwinJdevops Sep 18, 2026
705e5e8
fix: run evidence analyzer from repository import path
EdwinJdevops Sep 18, 2026
b2b9900
docs: define real AWS drift proof contract
EdwinJdevops Sep 18, 2026
d7a00ac
fix: expose repository package path to AWS proof analyzer
EdwinJdevops Sep 18, 2026
33758e3
docs: record verified real AWS drift proof
EdwinJdevops Sep 18, 2026
50b2b1b
feat: add conservative AWS cloud locators to evidence v1.1
EdwinJdevops Sep 18, 2026
737de67
fix: import environment module in AWS locator proof
EdwinJdevops Sep 18, 2026
df8cd94
feat: add fail-closed SSM CloudTrail attribution contract
EdwinJdevops Sep 18, 2026
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ GITHUB_APP_PRIVATE_KEY=
# Only needed for the multi-tenant SaaS flow — self-hosted single-account
# deployments using the ambient credential chain can leave this unset.
DRIFTGUARD_AWS_ACCOUNT_ID=
# Provider-native Evidence Bundle ingestion remains opt-in until the deployed
# database is explicitly migrated to the Evidence schema.
DRIFTGUARD_EVIDENCE_INGEST_ENABLED=false
ALLOWED_ORIGINS=http://localhost:3000
191 changes: 191 additions & 0 deletions .github/workflows/aws-real-provider-proof.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
name: Real AWS Provider Drift Proof

on:
push:
branches:
- feat/evidence-core-v1
paths:
- ".github/workflows/aws-real-provider-proof.yml"
- "tests/integration/aws_real/**"
- "backend/evidence/**"
workflow_dispatch:

permissions:
contents: read
id-token: write

concurrency:
group: driftguard-real-aws-provider-proof
cancel-in-progress: false

jobs:
aws-provider-proof:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
AWS_REGION: us-east-1
TF_IN_AUTOMATION: "true"
PARAMETER_NAME: /driftguard/proof/${{ github.run_id }}-${{ github.run_attempt }}

steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"

- name: Install exact Python dependencies used by the proof
run: python -m pip install --disable-pip-version-check pydantic==2.13.5 boto3==1.43.86

- name: Configure short-lived AWS credentials
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
with:
role-to-assume: arn:aws:iam::018724217910:role/DriftGuardGitHubProofRole
role-session-name: DriftGuardProof-${{ github.run_id }}
aws-region: ${{ env.AWS_REGION }}
role-duration-seconds: 900

- name: Set up Terraform 1.16.2
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: "1.16.2"
terraform_wrapper: false

- name: Verify AWS identity
run: |
python - <<'PY'
import boto3
identity = boto3.client("sts").get_caller_identity()
assert identity["Account"] == "018724217910"
assert ":assumed-role/DriftGuardGitHubProofRole/" in identity["Arn"]
print({"Account": identity["Account"], "Arn": identity["Arn"]})
PY

- name: Initialize exact AWS provider
working-directory: tests/integration/aws_real
run: terraform init -input=false

- name: Create disposable Terraform-managed parameter
working-directory: tests/integration/aws_real
run: terraform apply -input=false -auto-approve -var="parameter_name=${PARAMETER_NAME}"

- name: Verify baseline then mutate outside Terraform
run: |
python - <<'PY'
import os
import boto3

name = os.environ["PARAMETER_NAME"]
ssm = boto3.client("ssm", region_name=os.environ["AWS_REGION"])
before = ssm.get_parameter(Name=name)["Parameter"]["Value"]
assert before == "driftguard-baseline-value"
ssm.put_parameter(
Name=name,
Type="String",
Value="driftguard-out-of-band-value",
Overwrite=True,
)
after = ssm.get_parameter(Name=name)["Parameter"]["Value"]
assert after == "driftguard-out-of-band-value"
print({"parameter": name, "out_of_band_mutation_verified": True})
PY

- name: Produce provider-native refresh-only drift plan
working-directory: tests/integration/aws_real
run: |
terraform plan -refresh-only -input=false -out=drift.tfplan -var="parameter_name=${PARAMETER_NAME}"
terraform show -json drift.tfplan > raw-plan.json

- name: Produce redacted DriftGuard Evidence Bundle
working-directory: tests/integration/aws_real
run: |
PYTHONPATH="${GITHUB_WORKSPACE}" python -m backend.evidence raw-plan.json --engine terraform --output evidence.json

- name: Verify drift semantics and redaction
working-directory: tests/integration/aws_real
run: |
python - <<'PY'
import hashlib
import json
import os
from pathlib import Path

raw_bytes = Path("raw-plan.json").read_bytes()
evidence_bytes = Path("evidence.json").read_bytes()
raw = json.loads(raw_bytes)
evidence = json.loads(evidence_bytes)

assert b"driftguard-baseline-value" in raw_bytes
assert b"driftguard-out-of-band-value" in raw_bytes
assert b"driftguard-baseline-value" not in evidence_bytes
assert b"driftguard-out-of-band-value" not in evidence_bytes

drift = raw.get("resource_drift")
assert isinstance(drift, list) and len(drift) == 1

findings = evidence.get("findings")
assert isinstance(findings, list) and len(findings) == 1
finding = findings[0]
assert finding["resource_address"] == "aws_ssm_parameter.proof"
assert finding["resource_type"] == "aws_ssm_parameter"
assert "update" in finding["actions"]
assert "/value" in finding["changed_paths"]
assert evidence["schema_version"] == "1.1"
assert evidence["iac_engine"] == "terraform"
assert evidence["iac_engine_version"] == "1.16.2"
assert evidence["redaction_policy"] == "omit_change_values"

locator = finding["cloud_locator"]
assert locator["provider"] == "aws"
assert locator["name"] == os.environ["PARAMETER_NAME"]
assert locator["id"] == os.environ["PARAMETER_NAME"]
assert locator["region"] == os.environ["AWS_REGION"]
assert locator["arn"].endswith(os.environ["PARAMETER_NAME"].removeprefix("/"))

print(json.dumps({
"resource_address": finding["resource_address"],
"actions": finding["actions"],
"changed_paths": finding["changed_paths"],
"sensitive_paths": finding["sensitive_paths"],
"unknown_paths": finding["unknown_paths"],
"evidence_sha256": hashlib.sha256(evidence_bytes).hexdigest(),
"raw_plan_sha256": hashlib.sha256(raw_bytes).hexdigest(),
"redaction_verified": True,
}, sort_keys=True))
PY

- name: Destroy Terraform-managed proof resource
if: always()
working-directory: tests/integration/aws_real
run: |
terraform destroy -input=false -auto-approve -var="parameter_name=${PARAMETER_NAME}" || true

- name: Force cleanup and verify absence
if: always()
run: |
python - <<'PY'
import os
import boto3
from botocore.exceptions import ClientError

name = os.environ["PARAMETER_NAME"]
ssm = boto3.client("ssm", region_name=os.environ["AWS_REGION"])
try:
ssm.delete_parameter(Name=name)
except ClientError as exc:
if exc.response["Error"]["Code"] != "ParameterNotFound":
raise

try:
ssm.get_parameter(Name=name)
except ClientError as exc:
assert exc.response["Error"]["Code"] == "ParameterNotFound"
print({"parameter": name, "cleanup_verified": True})
else:
raise AssertionError("Disposable DriftGuard proof parameter still exists.")
PY

- name: Remove raw plan material from runner
if: always()
working-directory: tests/integration/aws_real
run: rm -f raw-plan.json drift.tfplan terraform.tfstate terraform.tfstate.backup evidence.json
54 changes: 45 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
cache: pip
Expand All @@ -22,21 +25,54 @@ jobs:
cli-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
cache: pip
- run: pip install -e ./cli
- run: pip install pytest ruff
- run: pip install pytest==9.1.1 ruff==0.16.5
- run: ruff check cli/driftguard_cli/ cli/tests/
- run: pytest cli/tests/ -v --tb=short

provider-drift-contract:
name: provider-drift-contract (${{ matrix.engine }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
engine: [terraform, opentofu]
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.12"
- name: Install evidence-core Python dependency
run: python -m pip install --disable-pip-version-check pydantic==2.13.5
- name: Set up Terraform 1.16.2
if: matrix.engine == 'terraform'
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
with:
terraform_version: "1.16.2"
terraform_wrapper: false
- name: Set up OpenTofu 1.12.6
if: matrix.engine == 'opentofu'
uses: opentofu/setup-opentofu@a1320f892987e89d278cc92dc5adc984fb93aca4 # v2.0.2
with:
tofu_version: "1.12.6"
tofu_wrapper: false
- name: Verify provider-native drift contract with Terraform
if: matrix.engine == 'terraform'
run: ./scripts/run_provider_drift_contract.sh terraform terraform
- name: Verify provider-native drift contract with OpenTofu
if: matrix.engine == 'opentofu'
run: ./scripts/run_provider_drift_contract.sh opentofu tofu

vscode-extension-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- working-directory: vscode-extension
Expand All @@ -47,8 +83,8 @@ jobs:
frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: "22"
- working-directory: frontend
Expand Down
38 changes: 38 additions & 0 deletions alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
[alembic]
script_location = backend/migrations
prepend_sys_path = .
path_separator = os

[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
Loading
Loading