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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/alertdesk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ name: AlertDesk

on:
push:
branches: [main, "intern/**"]
branches: [main, "intern/**", "interns/**"]
paths:
- "alertdesk/**"
- ".github/workflows/alertdesk.yml"
pull_request:
branches: [main, "intern/**"]
branches: [main, "intern/**", "interns/**"]
paths:
- "alertdesk/**"
- ".github/workflows/alertdesk.yml"
Expand Down Expand Up @@ -38,7 +38,6 @@ jobs:
container-scan:
runs-on: ubuntu-latest
needs: [test]
if: hashFiles('alertdesk/Dockerfile') != ''
steps:
- uses: actions/checkout@v4
- run: docker build -t alertdesk:ci .
Expand Down
14 changes: 14 additions & 0 deletions alertdesk/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.git
.venv
venv
__pycache__
*.py[cod]
*.egg-info
.pytest_cache
.coverage
htmlcov
tests
data/*
!data/.gitkeep
*.db
*.sqlite
21 changes: 21 additions & 0 deletions alertdesk/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

WORKDIR /app

COPY pyproject.toml README.md ./
COPY alertdesk ./alertdesk
RUN pip install --no-cache-dir . \
&& groupadd --system app \
&& useradd --system --gid app --create-home app \
&& mkdir -p /app/data \
&& chown app:app /app/data

COPY config ./config

USER app
EXPOSE 8000

CMD ["uvicorn", "alertdesk.app:app", "--host", "0.0.0.0", "--port", "8000"]
7 changes: 7 additions & 0 deletions alertdesk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ curl -s -H "Authorization: Bearer analyst-token" \

Lead token `lead-token` may transition; `analyst-token` may not.

## Run with Docker Compose

From this directory, run `docker compose up --build`. Compose exposes the API
on port 8000 and keeps SQLite data in the named `alertdesk-data` volume. See
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) for persistence and schema-change
notes.

## Git / PR workflow

| Work | PR target |
Expand Down
59 changes: 47 additions & 12 deletions alertdesk/SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,64 @@
# Security Policy

> **Week 2 task:** Replace the placeholders below with your own policy.
> Delete this callout when you commit the finished file.

## Reporting a vulnerability

<!-- TODO: How should someone report a vulnerability in AlertDesk itself? -->
If you find a vulnerability in AlertDesk, report it privately to the
maintainer:

- **Name:** George
- **Email:** baningeorge@gmail.com

[Describe your reporting process here.]
Do not publicly disclose vulnerabilities or include sensitive information
in public issues.

## Authentication (scaffold vs Month 4)

<!-- TODO: Document that tokens in client-spec.json are a stub, not production auth -->
<!-- Cover: never use these tokens outside the lab; replace in Month 4 -->
The current lab uses stub bearer tokens for authentication.

The lab tokens and their associated roles are defined in:

[Describe auth handling here.]
`config/client-spec.json`

These tokens are intended for the internship lab environment only and
must not be reused as production credentials.

Proper authentication is planned for Month 4.

## Database and audit log

<!-- TODO: Document ALERTDESK_DB, append-only audit_events, backups -->
AlertDesk stores ticket data in the configured SQLite database.

The database location is controlled by the `ALERTDESK_DB` environment
variable.

[Describe data handling here.]
Security-relevant actions are recorded in the `audit_events` table. Ticket
creation, assignment, and status transitions are audited. While the SQLite
`BEFORE UPDATE` and `BEFORE DELETE` triggers remain in place, they block
ordinary attempts to modify or delete audit records. This is not tamper-proof
protection against someone with sufficient access to the database file or
schema.

Audit logs must not contain passwords, bearer tokens, or other sensitive
secrets.

Database backups should be performed according to the deployment
environment's backup and recovery requirements.

## Scope limitations

<!-- TODO: AlertDesk is not a full SOAR/SIEM; known OWASP gaps in the stub -->
This security policy applies to the AlertDesk internship/scaffold
environment.

AlertDesk is **not a SOAR (Security Orchestration, Automation and
Response) platform or a SIEM (Security Information and Event Management)
platform**.

The scaffold uses stub tokens for testing only and should not be used
for real security incidents or production security operations.

The following are outside the current scaffold scope:

[Describe scope and limitations here.]
- Slack integration
- SIEM connectors
- SSO
- Multi-region operation
5 changes: 5 additions & 0 deletions alertdesk/alertdesk/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
from pydantic import BaseModel, Field

from . import db
from .observability import RequestLoggingMiddleware, configure_logging
from .auth import require
from .spec import can_transition

configure_logging()


@asynccontextmanager
async def lifespan(_app: FastAPI):
Expand All @@ -28,6 +31,8 @@ async def lifespan(_app: FastAPI):
lifespan=lifespan,
)

app.add_middleware(RequestLoggingMiddleware)


class TicketCreate(BaseModel):
title: str = Field(min_length=1)
Expand Down
12 changes: 12 additions & 0 deletions alertdesk/alertdesk/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ def init_schema(conn: sqlite3.Connection) -> None:
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (ticket_id) REFERENCES tickets(id)
);

CREATE TRIGGER IF NOT EXISTS audit_events_no_update
BEFORE UPDATE ON audit_events
BEGIN
SELECT RAISE(ABORT, 'audit_events rows are append-only');
END;

CREATE TRIGGER IF NOT EXISTS audit_events_no_delete
BEFORE DELETE ON audit_events
BEGIN
SELECT RAISE(ABORT, 'audit_events rows are append-only');
END;
"""
)
conn.commit()
58 changes: 58 additions & 0 deletions alertdesk/alertdesk/observability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Structured request logging that excludes credentials and request bodies."""

from __future__ import annotations

import json
import logging
import time
from typing import Any

from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response


class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
fields: dict[str, Any] = {
"timestamp": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
"level": record.levelname.lower(),
"logger": record.name,
"message": record.getMessage(),
}
fields.update(getattr(record, "event_fields", {}))
return json.dumps(fields, separators=(",", ":"), default=str)


def configure_logging() -> None:
logger = logging.getLogger("alertdesk.http")
if not any(getattr(handler, "_alertdesk_json", False) for handler in logger.handlers):
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
handler._alertdesk_json = True # type: ignore[attr-defined]
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.propagate = False


class RequestLoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(
self, request: Request, call_next: RequestResponseEndpoint
) -> Response:
started = time.perf_counter()
status_code = 500
try:
response = await call_next(request)
status_code = response.status_code
return response
finally:
elapsed_ms = round((time.perf_counter() - started) * 1000, 3)
logging.getLogger("alertdesk.http").info(
"http_request",
extra={"event_fields": {
"method": request.method,
"path": request.url.path,
"status_code": status_code,
"duration_ms": elapsed_ms,
}},
)
12 changes: 12 additions & 0 deletions alertdesk/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
alertdesk:
build:
context: .
ports:
- "127.0.0.1:8001:8000"
volumes:
- alertdesk-data:/app/data
restart: unless-stopped

volumes:
alertdesk-data:
44 changes: 44 additions & 0 deletions alertdesk/docs/BENCHMARKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Week 5 performance baseline and observability

## Benchmark

`scripts/benchmark.py` measures end-to-end HTTP latency for ticket creation and
listing against a running AlertDesk instance. It performs five warm-up pairs by
default, then records 100 measured create/list pairs. Results report mean,
median, p95, minimum, and maximum latency in milliseconds as JSON. The script
uses Python's standard library and the analyst bearer token configured by the
sample client spec.

Start the service in one terminal with `python -m alertdesk.cli`, then run
`python scripts/benchmark.py` in another. Options include `--base-url`, `--token`,
`--iterations`, and `--warmup`. Benchmark records use unique synthetic titles
and are persisted by the target service; use a disposable database for repeatable
runs. Listing time naturally rises as the table grows because the endpoint returns
all tickets. Results depend on machine load, database size, and whether the API
is local, so compare runs only under similar conditions.

## Baseline

No latency target is specified in the client brief. Capture a baseline on the
intended development environment and append the command, date, database size,
iteration count, and emitted JSON summary here. Do not treat measurements from a
single developer machine as a production service-level objective.

## Request logging

The API emits one JSON log event per HTTP request with timestamp, method, URL
path, status code, and elapsed milliseconds. The logger intentionally excludes
headers, bearer tokens, query strings, request bodies, and ticket data. These
fields are enough to identify slow routes and error rates without copying alert
contents into general application logs. Logs go to stderr through Python's
standard logging handler and can be collected by the process supervisor or
container runtime.

## Operational notes

- The benchmark includes HTTP client, API, SQLite, and response serialization time.
- The list endpoint has no pagination, so its latency and response size grow with
the number of stored tickets.
- The benchmark creates records and does not clean them up; point it at a
disposable database when measuring repeatedly.
- This is a local development baseline, not a capacity or concurrency test.
35 changes: 35 additions & 0 deletions alertdesk/docs/DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Local container deployment

From the `alertdesk/` directory, start the API with:

```bash
docker compose up --build
```

The API is available at <http://localhost:8000/docs>. Compose stores SQLite's
`data/alertdesk.sqlite` in the named `alertdesk-data` volume, so recreating the
container does not remove ticket data. Back up that volume before upgrading or
removing it. `docker compose down -v` deletes the volume and its data.

The image runs Uvicorn as the unprivileged `app` user. The application image
contains the runtime client specification and creates the SQLite database on
startup.

## Schema changes

Week 7 does not add or change database columns, so no migration is needed for
this release. Before a future column change, add a versioned migration that
records the current schema version, applies the new schema in a transaction,
and is safe to rerun. Back up the SQLite volume before applying it; test both a
fresh database and a copy of the prior release's database.

## Request logging

The API writes one structured JSON log event for each HTTP request. Each event
includes the request method, URL path, response status code, and duration in
milliseconds. The middleware does not log credentials or request bodies. These logs
are emitted to the container's standard error stream (stderr) and can be
collected by the container runtime's logging system.

The CI `container-scan` job builds this Dockerfile and fails on Trivy HIGH or

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The container-scan job in .github/workflows/alertdesk.yml only runs for intern/**. This PR targets interns/Evelyn-Banin, so Trivy does not run here. Add interns/** to the push and pull_request branch filters. Also run docker compose up --build once and confirm the app user can create the SQLite file on a fresh volume; the description says Docker was not available when this was written.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also fixed the GitHub Actions workflow configuration. The latest workflow run (#90) is currently awaiting maintainer approval.

Could you please approve the workflow run so the tests and container scan can execute?

Thank you.

CRITICAL findings.
Loading