wafpass-server 0.3.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version as _pkg_version
2
+
3
+ try:
4
+ __version__ = _pkg_version("wafpass-server")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.3.0-dev"
@@ -0,0 +1,21 @@
1
+ """Application configuration via environment variables."""
2
+ from __future__ import annotations
3
+
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
9
+
10
+ database_url: str = "postgresql+asyncpg://wafpass:wafpass@localhost:5432/wafpass"
11
+ wafpass_env: str = "local"
12
+
13
+ # CORS origins (comma-separated)
14
+ cors_origins: str = "http://localhost:5173,http://localhost:3000"
15
+
16
+ @property
17
+ def cors_origins_list(self) -> list[str]:
18
+ return [o.strip() for o in self.cors_origins.split(",") if o.strip()]
19
+
20
+
21
+ settings = Settings()
@@ -0,0 +1,21 @@
1
+ """Async SQLAlchemy engine and session factory."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import AsyncGenerator
5
+
6
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
7
+ from sqlalchemy.orm import DeclarativeBase
8
+
9
+ from wafpass_server.config import settings
10
+
11
+ engine = create_async_engine(settings.database_url, echo=False, pool_pre_ping=True)
12
+ AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
13
+
14
+
15
+ class Base(DeclarativeBase):
16
+ pass
17
+
18
+
19
+ async def get_db() -> AsyncGenerator[AsyncSession, None]:
20
+ async with AsyncSessionLocal() as session:
21
+ yield session
wafpass_server/main.py ADDED
@@ -0,0 +1,46 @@
1
+ """WAF++ PASS server entry point."""
2
+ from __future__ import annotations
3
+
4
+ import uvicorn
5
+ from fastapi import FastAPI
6
+ from fastapi.middleware.cors import CORSMiddleware
7
+
8
+ from wafpass_server.config import settings
9
+ from wafpass_server.routers.controls import router as controls_router
10
+ from wafpass_server.routers.runs import router as runs_router
11
+
12
+ app = FastAPI(
13
+ title="wafpass-server",
14
+ version="0.3.0",
15
+ description="REST API for persisting and querying WAF++ PASS scan results.",
16
+ docs_url="/api/docs",
17
+ redoc_url="/api/redoc",
18
+ openapi_tags=[
19
+ {"name": "runs", "description": "Scan run results ingestion and retrieval."},
20
+ {"name": "controls", "description": "WAF++ control catalogue management."},
21
+ ],
22
+ )
23
+
24
+ app.add_middleware(
25
+ CORSMiddleware,
26
+ allow_origins=settings.cors_origins_list,
27
+ allow_credentials=True,
28
+ allow_methods=["*"],
29
+ allow_headers=["*"],
30
+ )
31
+
32
+ app.include_router(runs_router)
33
+ app.include_router(controls_router)
34
+
35
+
36
+ @app.get("/health", tags=["health"])
37
+ async def health() -> dict[str, str]:
38
+ return {"status": "ok"}
39
+
40
+
41
+ def start() -> None:
42
+ uvicorn.run("wafpass_server.main:app", host="0.0.0.0", port=8000, reload=False)
43
+
44
+
45
+ if __name__ == "__main__":
46
+ start()
@@ -0,0 +1,51 @@
1
+ """SQLAlchemy ORM models."""
2
+ from __future__ import annotations
3
+
4
+ import uuid
5
+ from datetime import datetime, timezone
6
+
7
+ from sqlalchemy import DateTime, Integer, Text
8
+ from sqlalchemy.dialects.postgresql import JSONB, UUID
9
+ from sqlalchemy.orm import Mapped, mapped_column
10
+
11
+ from wafpass_server.database import Base
12
+
13
+
14
+ def _now() -> datetime:
15
+ return datetime.now(timezone.utc)
16
+
17
+
18
+ class Control(Base):
19
+ __tablename__ = "controls"
20
+
21
+ id: Mapped[str] = mapped_column(Text, primary_key=True)
22
+ pillar: Mapped[str] = mapped_column(Text, default="")
23
+ severity: Mapped[str] = mapped_column(Text, default="")
24
+ type: Mapped[list] = mapped_column(JSONB, default=list)
25
+ description: Mapped[str] = mapped_column(Text, default="")
26
+ checks: Mapped[list] = mapped_column(JSONB, default=list)
27
+ source: Mapped[str] = mapped_column(Text, default="wafpass")
28
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
29
+ updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
30
+
31
+
32
+ class Run(Base):
33
+ __tablename__ = "runs"
34
+
35
+ id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
36
+ project: Mapped[str] = mapped_column(Text, default="")
37
+ branch: Mapped[str] = mapped_column(Text, default="")
38
+ git_sha: Mapped[str] = mapped_column(Text, default="")
39
+ triggered_by: Mapped[str] = mapped_column(Text, default="local")
40
+ iac_framework: Mapped[str] = mapped_column(Text, default="terraform")
41
+ score: Mapped[int] = mapped_column(Integer, default=0)
42
+ pillar_scores: Mapped[dict] = mapped_column(JSONB, default=dict)
43
+ findings: Mapped[list] = mapped_column(JSONB, default=list)
44
+ path: Mapped[str] = mapped_column(Text, default="")
45
+ controls_loaded: Mapped[int] = mapped_column(Integer, default=0)
46
+ controls_run: Mapped[int] = mapped_column(Integer, default=0)
47
+ detected_regions: Mapped[list] = mapped_column(JSONB, default=list)
48
+ source_paths: Mapped[list] = mapped_column(JSONB, default=list)
49
+ controls_meta: Mapped[list] = mapped_column(JSONB, default=list)
50
+ plan_changes: Mapped[dict | None] = mapped_column(JSONB, nullable=True, default=None)
51
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_now)
File without changes
@@ -0,0 +1,119 @@
1
+ """POST/GET/DELETE /controls endpoints."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Annotated
5
+
6
+ from fastapi import APIRouter, Depends, HTTPException, Query
7
+ from sqlalchemy import func, select
8
+ from sqlalchemy.ext.asyncio import AsyncSession
9
+
10
+ from wafpass_server.database import get_db
11
+ from wafpass_server.models import Control, _now
12
+ from wafpass_server.schemas import ControlIn, ControlOut, Envelope, Meta
13
+
14
+ router = APIRouter(prefix="/controls", tags=["controls"])
15
+
16
+
17
+ # ── Helpers ───────────────────────────────────────────────────────────────────
18
+
19
+
20
+ def _to_out(ctrl: Control) -> ControlOut:
21
+ return ControlOut.model_validate(ctrl, from_attributes=True)
22
+
23
+
24
+ # ── Endpoints ─────────────────────────────────────────────────────────────────
25
+
26
+
27
+ @router.post("", response_model=Envelope[ControlOut], status_code=200)
28
+ async def upsert_control(
29
+ payload: ControlIn,
30
+ db: Annotated[AsyncSession, Depends(get_db)],
31
+ ) -> Envelope[ControlOut]:
32
+ """Create or update a control (idempotent upsert on ``id``)."""
33
+ ctrl = await db.get(Control, payload.id)
34
+
35
+ if ctrl is None:
36
+ ctrl = Control(
37
+ id=payload.id,
38
+ pillar=payload.pillar,
39
+ severity=payload.severity,
40
+ type=list(payload.type),
41
+ description=payload.description,
42
+ checks=[c.model_dump() for c in payload.checks],
43
+ source=payload.source,
44
+ )
45
+ db.add(ctrl)
46
+ else:
47
+ ctrl.pillar = payload.pillar
48
+ ctrl.severity = payload.severity
49
+ ctrl.type = list(payload.type)
50
+ ctrl.description = payload.description
51
+ ctrl.checks = [c.model_dump() for c in payload.checks]
52
+ ctrl.source = payload.source
53
+ ctrl.updated_at = _now()
54
+
55
+ await db.commit()
56
+ await db.refresh(ctrl)
57
+ return Envelope(data=_to_out(ctrl))
58
+
59
+
60
+ @router.get("", response_model=Envelope[list[ControlOut]])
61
+ async def list_controls(
62
+ db: Annotated[AsyncSession, Depends(get_db)],
63
+ pillar: str | None = Query(default=None, description="Filter by pillar name."),
64
+ severity: str | None = Query(default=None, description="Filter by severity level."),
65
+ page: int = Query(default=1, ge=1, description="1-based page number."),
66
+ per_page: int = Query(default=50, ge=1, le=200, description="Results per page."),
67
+ ) -> Envelope[list[ControlOut]]:
68
+ """List controls, optionally filtered by pillar and/or severity."""
69
+ base = select(Control)
70
+ count_base = select(func.count()).select_from(Control)
71
+
72
+ if pillar:
73
+ base = base.where(Control.pillar == pillar.lower())
74
+ count_base = count_base.where(Control.pillar == pillar.lower())
75
+ if severity:
76
+ base = base.where(Control.severity == severity.lower())
77
+ count_base = count_base.where(Control.severity == severity.lower())
78
+
79
+ total: int = (await db.execute(count_base)).scalar() or 0
80
+
81
+ offset = (page - 1) * per_page
82
+ stmt = base.order_by(Control.created_at.desc()).limit(per_page).offset(offset)
83
+ result = await db.execute(stmt)
84
+ controls = list(result.scalars().all())
85
+
86
+ return Envelope(
87
+ data=[_to_out(c) for c in controls],
88
+ meta=Meta(total=total, page=page, per_page=per_page),
89
+ )
90
+
91
+
92
+ @router.get("/{control_id}", response_model=Envelope[ControlOut])
93
+ async def get_control(
94
+ control_id: str,
95
+ db: Annotated[AsyncSession, Depends(get_db)],
96
+ ) -> Envelope[ControlOut]:
97
+ """Return a single control by ID."""
98
+ ctrl = await db.get(Control, control_id.upper())
99
+ if ctrl is None:
100
+ # Also try as-provided (case-sensitive stored IDs)
101
+ ctrl = await db.get(Control, control_id)
102
+ if ctrl is None:
103
+ raise HTTPException(status_code=404, detail=f"Control '{control_id}' not found")
104
+ return Envelope(data=_to_out(ctrl))
105
+
106
+
107
+ @router.delete("/{control_id}", status_code=204)
108
+ async def delete_control(
109
+ control_id: str,
110
+ db: Annotated[AsyncSession, Depends(get_db)],
111
+ ) -> None:
112
+ """Remove a control by ID."""
113
+ ctrl = await db.get(Control, control_id.upper())
114
+ if ctrl is None:
115
+ ctrl = await db.get(Control, control_id)
116
+ if ctrl is None:
117
+ raise HTTPException(status_code=404, detail=f"Control '{control_id}' not found")
118
+ await db.delete(ctrl)
119
+ await db.commit()
@@ -0,0 +1,101 @@
1
+ """POST/GET /runs endpoints."""
2
+ from __future__ import annotations
3
+
4
+ import uuid
5
+ from typing import Annotated
6
+
7
+ from fastapi import APIRouter, Depends, HTTPException, Query
8
+ from sqlalchemy import select
9
+ from sqlalchemy.ext.asyncio import AsyncSession
10
+
11
+ from wafpass_server.database import get_db
12
+ from wafpass_server.models import Run
13
+ from wafpass_server.schemas import ControlMetaSchema, FindingSchema, RunCreate, RunDetail, RunSummary
14
+
15
+ router = APIRouter(prefix="/runs", tags=["runs"])
16
+
17
+
18
+ @router.post("", response_model=RunSummary, status_code=201)
19
+ async def create_run(
20
+ payload: RunCreate,
21
+ db: Annotated[AsyncSession, Depends(get_db)],
22
+ ) -> Run:
23
+ run = Run(
24
+ project=payload.project,
25
+ branch=payload.branch,
26
+ git_sha=payload.git_sha,
27
+ triggered_by=payload.triggered_by,
28
+ iac_framework=payload.iac_framework,
29
+ score=payload.score,
30
+ pillar_scores=payload.pillar_scores,
31
+ findings=[f.model_dump() for f in payload.findings],
32
+ path=payload.path,
33
+ controls_loaded=payload.controls_loaded,
34
+ controls_run=payload.controls_run,
35
+ detected_regions=payload.detected_regions,
36
+ source_paths=payload.source_paths,
37
+ controls_meta=[c.model_dump() for c in payload.controls_meta],
38
+ plan_changes=payload.plan_changes,
39
+ )
40
+ db.add(run)
41
+ await db.commit()
42
+ await db.refresh(run)
43
+ return run
44
+
45
+
46
+ @router.get("", response_model=list[RunSummary])
47
+ async def list_runs(
48
+ db: Annotated[AsyncSession, Depends(get_db)],
49
+ limit: int = Query(default=50, ge=1, le=200),
50
+ offset: int = Query(default=0, ge=0),
51
+ project: str | None = Query(default=None),
52
+ ) -> list[Run]:
53
+ stmt = select(Run).order_by(Run.created_at.desc()).limit(limit).offset(offset)
54
+ if project:
55
+ stmt = stmt.where(Run.project == project)
56
+ result = await db.execute(stmt)
57
+ return list(result.scalars().all())
58
+
59
+
60
+ @router.get("/{run_id}", response_model=RunDetail)
61
+ async def get_run(
62
+ run_id: uuid.UUID,
63
+ db: Annotated[AsyncSession, Depends(get_db)],
64
+ ) -> Run:
65
+ run = await db.get(Run, run_id)
66
+ if run is None:
67
+ raise HTTPException(status_code=404, detail="Run not found")
68
+ return run
69
+
70
+
71
+ @router.get("/{run_id}/controls", response_model=list[ControlMetaSchema])
72
+ async def get_controls(
73
+ run_id: uuid.UUID,
74
+ db: Annotated[AsyncSession, Depends(get_db)],
75
+ ) -> list[dict]:
76
+ run = await db.get(Run, run_id)
77
+ if run is None:
78
+ raise HTTPException(status_code=404, detail="Run not found")
79
+ return run.controls_meta or []
80
+
81
+
82
+ @router.get("/{run_id}/findings", response_model=list[FindingSchema])
83
+ async def get_findings(
84
+ run_id: uuid.UUID,
85
+ db: Annotated[AsyncSession, Depends(get_db)],
86
+ severity: str | None = Query(default=None),
87
+ pillar: str | None = Query(default=None),
88
+ status: str | None = Query(default=None),
89
+ ) -> list[dict]:
90
+ run = await db.get(Run, run_id)
91
+ if run is None:
92
+ raise HTTPException(status_code=404, detail="Run not found")
93
+
94
+ findings: list[dict] = run.findings or []
95
+ if severity:
96
+ findings = [f for f in findings if f.get("severity", "").upper() == severity.upper()]
97
+ if pillar:
98
+ findings = [f for f in findings if f.get("pillar", "").upper() == pillar.upper()]
99
+ if status:
100
+ findings = [f for f in findings if f.get("status", "").upper() == status.upper()]
101
+ return findings
@@ -0,0 +1,136 @@
1
+ """Pydantic schemas for the API layer."""
2
+ from __future__ import annotations
3
+
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Any, Generic, TypeVar
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+ # Re-export control schema types from wafpass-core so callers only need one import.
11
+ from wafpass.control_schema import WizardCheck, WizardControl # noqa: F401
12
+
13
+ # ── Generic response envelope ─────────────────────────────────────────────────
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ class Meta(BaseModel):
19
+ total: int | None = None
20
+ page: int | None = None
21
+ per_page: int | None = None
22
+
23
+
24
+ class Envelope(BaseModel, Generic[T]):
25
+ """Consistent API response wrapper used by all /controls endpoints."""
26
+
27
+ data: T
28
+ meta: Meta = Field(default_factory=Meta)
29
+
30
+
31
+ class FindingSchema(BaseModel):
32
+ check_id: str
33
+ check_title: str
34
+ control_id: str
35
+ pillar: str = ""
36
+ severity: str
37
+ status: str
38
+ resource: str
39
+ message: str
40
+ remediation: str
41
+ example: dict[str, Any] | None = None
42
+
43
+
44
+ class ControlCheckMetaSchema(BaseModel):
45
+ id: str
46
+ title: str
47
+ severity: str
48
+ remediation: str = ""
49
+ example: dict[str, Any] | None = None
50
+
51
+
52
+ class ControlMetaSchema(BaseModel):
53
+ id: str
54
+ title: str
55
+ pillar: str
56
+ severity: str
57
+ category: str = ""
58
+ description: str = ""
59
+ rationale: str = ""
60
+ threat: list[str] = Field(default_factory=list)
61
+ regulatory_mapping: list[dict[str, Any]] = Field(default_factory=list)
62
+ checks: list[ControlCheckMetaSchema] = Field(default_factory=list)
63
+
64
+
65
+ class RunCreate(BaseModel):
66
+ """Payload accepted by POST /runs — matches wafpass-result.json schema."""
67
+ schema_version: str = "1.0"
68
+ project: str = ""
69
+ branch: str = ""
70
+ git_sha: str = ""
71
+ triggered_by: str = "local"
72
+ iac_framework: str = "terraform"
73
+ score: int = Field(default=0, ge=0, le=100)
74
+ pillar_scores: dict[str, int] = Field(default_factory=dict)
75
+ path: str = ""
76
+ controls_loaded: int = 0
77
+ controls_run: int = 0
78
+ detected_regions: list[list[str]] = Field(default_factory=list)
79
+ source_paths: list[str] = Field(default_factory=list)
80
+ controls_meta: list[ControlMetaSchema] = Field(default_factory=list)
81
+ findings: list[FindingSchema] = Field(default_factory=list)
82
+ plan_changes: dict[str, Any] | None = None
83
+
84
+
85
+ class RunSummary(BaseModel):
86
+ id: uuid.UUID
87
+ project: str
88
+ branch: str
89
+ git_sha: str
90
+ triggered_by: str
91
+ iac_framework: str
92
+ score: int
93
+ pillar_scores: dict[str, int]
94
+ path: str
95
+ controls_loaded: int
96
+ controls_run: int
97
+ created_at: datetime
98
+
99
+ model_config = {"from_attributes": True}
100
+
101
+
102
+ class RunDetail(RunSummary):
103
+ findings: list[dict[str, Any]]
104
+ detected_regions: list[list[str]]
105
+ source_paths: list[str]
106
+ controls_meta: list[dict[str, Any]]
107
+ plan_changes: dict[str, Any] | None = None
108
+
109
+ model_config = {"from_attributes": True}
110
+
111
+
112
+ # ── Control schemas ───────────────────────────────────────────────────────────
113
+
114
+
115
+ class ControlIn(WizardControl):
116
+ """Request body for POST /controls.
117
+
118
+ Extends WizardControl (from wafpass-core) with an optional ``source``
119
+ field indicating the authoring origin.
120
+ """
121
+
122
+ source: str = "wafpass"
123
+
124
+
125
+ class ControlOut(WizardControl):
126
+ """Response schema for /controls endpoints.
127
+
128
+ Extends WizardControl with server-managed timestamp fields.
129
+ ``from_attributes=True`` enables construction from SQLAlchemy ORM rows.
130
+ """
131
+
132
+ source: str
133
+ created_at: datetime
134
+ updated_at: datetime
135
+
136
+ model_config = ConfigDict(from_attributes=True)
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: wafpass-server
3
+ Version: 0.3.4
4
+ Summary: WAF++ PASS – API server for persisting and querying scan results
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: alembic>=1.13
8
+ Requires-Dist: asyncpg>=0.29
9
+ Requires-Dist: fastapi>=0.100
10
+ Requires-Dist: pydantic-settings>=2.0
11
+ Requires-Dist: pydantic>=2.0
12
+ Requires-Dist: python-dotenv>=1.0
13
+ Requires-Dist: sqlalchemy>=2.0
14
+ Requires-Dist: uvicorn[standard]>=0.23
15
+ Provides-Extra: dev
16
+ Requires-Dist: httpx>=0.26; extra == 'dev'
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
18
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
19
+ Requires-Dist: pytest>=7.0; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ # wafpass-server
23
+
24
+ REST API for persisting and querying WAF++ PASS scan results.
25
+
26
+ Receives `wafpass-result.json` payloads from `wafpass check --output json`,
27
+ stores them in PostgreSQL, and exposes them to the dashboard and CI tooling.
28
+
29
+ ## API endpoints
30
+
31
+ | Method | Path | Description |
32
+ |--------|------|-------------|
33
+ | `POST` | `/runs` | Ingest a `wafpass-result.json` payload |
34
+ | `GET` | `/runs` | List runs (query: `limit`, `offset`, `project`) |
35
+ | `GET` | `/runs/{id}` | Single run with all findings |
36
+ | `GET` | `/runs/{id}/findings` | Findings only (query: `severity`, `pillar`, `status`) |
37
+ | `GET` | `/health` | Health check |
38
+ | `GET` | `/api/docs` | Swagger UI |
39
+
40
+ ## Setup
41
+
42
+ ### Environment variables
43
+
44
+ Copy `.env.example` from the repo root:
45
+
46
+ ```
47
+ DATABASE_URL=postgresql+asyncpg://wafpass:changeme@localhost:5432/wafpass
48
+ WAFPASS_ENV=local
49
+ CORS_ORIGINS=http://localhost:5173,http://localhost:3000
50
+ ```
51
+
52
+ ### Run locally
53
+
54
+ ```bash
55
+ pip install -e ".[dev]"
56
+ alembic upgrade head
57
+ uvicorn wafpass_server.main:app --reload --port 8000
58
+ ```
59
+
60
+ ### Run migrations
61
+
62
+ ```bash
63
+ alembic upgrade head # apply all migrations
64
+ alembic downgrade -1 # roll back one step
65
+ alembic revision --autogenerate -m "add column" # generate new migration
66
+ ```
67
+
68
+ ### Docker
69
+
70
+ ```bash
71
+ docker build -t wafpass-server .
72
+ docker run -e DATABASE_URL=... -p 8000:8000 wafpass-server
73
+ ```
74
+
75
+ ### docker-compose (full stack)
76
+
77
+ From the repo root:
78
+
79
+ ```bash
80
+ cp .env.example .env # fill in passwords
81
+ docker compose up
82
+ ```
83
+
84
+ ## Posting a scan result
85
+
86
+ ```bash
87
+ wafpass check infra/ --output json > result.json
88
+ curl -X POST http://localhost:8000/runs \
89
+ -H "Content-Type: application/json" \
90
+ -d @result.json
91
+ ```
92
+
93
+ Or set metadata fields before posting:
94
+
95
+ ```python
96
+ import json, httpx
97
+
98
+ result = json.load(open("result.json"))
99
+ result.update({"project": "my-infra", "branch": "main", "git_sha": "abc1234"})
100
+ httpx.post("http://localhost:8000/runs", json=result)
101
+ ```
102
+
103
+ ## Result schema
104
+
105
+ The payload shape is defined by `WafpassResultSchema` in `wafpass-core`
106
+ (`wafpass/schema.py`). `wafpass-server` mirrors that schema in
107
+ `wafpass_server/schemas.py` (`RunCreate`). Once `wafpass-core` is published
108
+ to PyPI, replace the local definition with a direct import.
109
+
110
+ Key fields stored per run:
111
+
112
+ | Column | Type | Description |
113
+ |--------|------|-------------|
114
+ | `id` | uuid | Auto-generated primary key |
115
+ | `project` | text | Repo / project name |
116
+ | `branch` | text | VCS branch |
117
+ | `git_sha` | text | Commit SHA |
118
+ | `triggered_by` | text | `local` \| `github-actions` \| `gitlab-ci` \| … |
119
+ | `iac_framework` | text | `terraform` \| `cdk` \| … |
120
+ | `score` | int | Overall compliance score (0–100) |
121
+ | `pillar_scores` | jsonb | Per-pillar scores `{"SEC": 90, …}` |
122
+ | `findings` | jsonb | Array of check results |
123
+ | `created_at` | timestamptz | Inserted at |
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ pip install -e ".[dev]"
129
+ pytest
130
+ ```
@@ -0,0 +1,13 @@
1
+ wafpass_server/__init__.py,sha256=9EoKGeTrY5JnBKcERuTMzJtP34xpSQ0RqD2dOJv52es,191
2
+ wafpass_server/config.py,sha256=XU8ea65sgb-oQIPO-kBBb_hfzJAWouvesmKjCYaOwyQ,650
3
+ wafpass_server/database.py,sha256=uecjB4WgDW7ujKMpcpcHdozlDTO7HqX4vjyGK--xPd0,632
4
+ wafpass_server/main.py,sha256=pMtQqFhVu5-hKpH8ywTWJE45wOCOwsTYdfNVITVCPH0,1230
5
+ wafpass_server/models.py,sha256=mUYvO8cZ6Dn9gDjJjQPQDB8mH1hfThCrFoNHwFR1v1g,2270
6
+ wafpass_server/schemas.py,sha256=to39HnfYpNi7dxElrOHwZCneT8Ffzk4aMdy6G2vdoAI,3784
7
+ wafpass_server/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ wafpass_server/routers/controls.py,sha256=O2CoIGWWNeT81Qw4pO5HnUxIvG1brk-Th-wu7wbH6ys,4491
9
+ wafpass_server/routers/runs.py,sha256=y_etZa7h7tbmKJobY8KHlVAI6FLQrzB-uk8Z4WD76cU,3386
10
+ wafpass_server-0.3.4.dist-info/METADATA,sha256=4UT9JCSCp646NYyWaJ4Lh_9FLpcq19OMt2AVg6tggIA,3549
11
+ wafpass_server-0.3.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
12
+ wafpass_server-0.3.4.dist-info/entry_points.txt,sha256=1PNS1zNpHTnOPK6zZ8AwIBMXndvXpPVnXieBRVPRlNE,61
13
+ wafpass_server-0.3.4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wafpass-server = wafpass_server.main:start