From cf98eb26730493bc83fa9345512ab5e4e7a117b5 Mon Sep 17 00:00:00 2001 From: vanna-cloud Date: Tue, 22 Sep 2026 00:28:30 -1200 Subject: [PATCH 01/17] Create spec.md --- alertdesk/docs/spec.md | 139 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 alertdesk/docs/spec.md diff --git a/alertdesk/docs/spec.md b/alertdesk/docs/spec.md new file mode 100644 index 0000000..b1af594 --- /dev/null +++ b/alertdesk/docs/spec.md @@ -0,0 +1,139 @@ +AlertDesk Specification + +1. Purpose + +AlertDesk is a web service that security analysts use to file and progress security alerts through a controlled ticket workflow. + +2. API Resources + +The main API resource is tickets. A ticket contains: + +title + +description + +severity + +Supported ticket operations include creating, listing, retrieving, and assigning tickets. Authorized roles may also change ticket status. + +3. Severity + +Severity values are client-defined. The current sample values are: + +low + +medium + +high + +critical + +The implementation must read client-defined values from configuration rather than hardcoding them in Python. + +4. Ticket Status Workflow + +The current sample statuses are: + +new -> triaged -> assigned -> resolved -> closed + +Not every transition is legal. Legal transitions must come from config/client-spec.json. The application must reject transitions that are not defined as legal. + +5. Roles and Permissions + +Role + +Create + +List/Get + +Assign + +Change Status + +Analyst + +Yes + +Yes + +Yes + +No + +Lead + +Yes + +Yes + +Yes + +Yes + +Admin + +Yes + +Yes + +Yes + +Yes + +These permissions are client-defined and must remain configuration-driven. + +6. Acceptance Criteria + +An authorized analyst can create a ticket with a title, description and valid severity. + +An authorized analyst can list tickets. + +An authorized user can retrieve an individual ticket. + +An authorized analyst can assign a ticket to an analyst identifier such as an email or handle. + +Only authorized roles can change ticket status. + +Illegal status transitions are rejected. + +Creating, assigning and transitioning a ticket append audit records. + +The running service exposes OpenAPI documentation. + +7. Audit Requirements + +The audit trail is append-only and records who performed important actions and when. At minimum, the following actions are audited: + +ticket creation + +ticket assignment + +ticket status transition + +8. Threat / Abuse Notes + +Potential abuse cases include: + +spoofed or invalid bearer tokens + +attempting actions outside the authenticated user's permissions + +attempting illegal ticket status transitions + +exposing sensitive information through audit records + +bypassing configuration-driven workflow rules through hardcoded values + +9. Out of Scope for the Scaffold + +Slack integration + +SIEM connectors + +SSO + +multi-region operation + +10. Configuration Rule + +Statuses, roles, severity values and legal transitions must be read from the client specification/configuration. If the client changes the workflow, such as adding waiting-vendor, the configuration and tests should be updated rather than introducing hardcoded status enums in Python. From ffb16c4161635cd71b62735bb3eca6303c986d53 Mon Sep 17 00:00:00 2001 From: vanna-cloud Date: Tue, 22 Sep 2026 00:43:41 -1200 Subject: [PATCH 02/17] Create functional_requirements --- alertdesk/docs/functional_requirements | 179 +++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 alertdesk/docs/functional_requirements diff --git a/alertdesk/docs/functional_requirements b/alertdesk/docs/functional_requirements new file mode 100644 index 0000000..a41767f --- /dev/null +++ b/alertdesk/docs/functional_requirements @@ -0,0 +1,179 @@ +1. Purpose + +This document defines the functional requirements for AlertDesk. These requirements describe the functions the service must provide based on the current client brief and configuration-driven workflow requirement. + +2. Functional Requirements + +ID + +Function Requirement + +FR-01 + +Create ticket + +The system shall allow an authorized analyst to create a security ticket. + +FR-02 + +Ticket information + +The system shall allow a ticket to contain a title, description, and severity. + +FR-03 + +Severity selection + +The system shall allow ticket severity to be selected from the values defined by the client specification. + +FR-04 + +List tickets + +The system shall allow an authorized analyst to list/retrieve security tickets. + +FR-05 + +Get ticket + +The system shall allow an authorized analyst to retrieve the details of an individual ticket. + +FR-06 + +Assign ticket + +The system shall allow an authorized analyst to assign a ticket to an analyst identifier, such as an email address or handle. + +FR-07 + +Change ticket status + +The system shall allow only authorized Lead and Admin roles to change a ticket status. + +FR-08 + +Validate status transition + +The system shall reject a ticket-status change when the transition is not defined as legal in the client specification. + +FR-09 + +Configuration-driven workflow + +The system shall obtain ticket statuses, roles, severity values, and legal status transitions from the client JSON specification rather than hardcoding them in Python. + +FR-10 + +Role-based authorization + +The system shall enforce permissions according to the authenticated user’s role. + +FR-11 + +Audit ticket creation + +The system shall record ticket-creation actions in an append-only audit trail. + +FR-12 + +Audit assignment + +The system shall record ticket-assignment actions in an append-only audit trail. + +FR-13 + +Audit status transition + +The system shall record ticket-status transitions in an append-only audit trail. + +FR-14 + +Audit actor and time + +The system shall record who performed an audited action and when it occurred. + +FR-15 + +API service + +The system shall expose the ticket functions through a web service/API rather than a CLI-only interface. + +FR-16 + +OpenAPI documentation + +The running application shall expose OpenAPI documentation describing the available API operations and schemas. + +3. Role Permissions + +Role Create List/Get Assign Change status + +Analyst Yes Yes Yes No + +Lead Yes Yes Yes Yes + +Admin Yes Yes Yes Yes + +4. Current Sample Workflow + +The current sample workflow is: + +New → Triaged → Assigned → Resolved → Closed + +The application shall not allow arbitrary status jumps. The legal transitions must be read from the JSON specification. + +5. Acceptance Criteria + +.Area + +.Acceptance criterion + +.Ticket creation + +An authorized analyst can create a ticket with a title, description, and a valid client-defined severity. + +Ticket retrieval + +An authorized analyst can list tickets and retrieve an individual ticket. + +Assignment + +An authorized analyst can assign a ticket to an analyst identifier. + +Status authorization + +An Analyst cannot change status; a Lead or Admin can change status when otherwise valid. + +Transition validation + +An attempted illegal status transition is rejected. + +Audit trail + +Creating, assigning, or transitioning a ticket creates an append-only audit record identifying the actor and time. + +Configuration + +Changing the client JSON specification can change supported statuses/roles/transitions without hardcoding those values in Python. + +OpenAPI + +The running service exposes its API contract through OpenAPI documentation. + +6. Functional Scope Exclusions + +Slack integration + +SIEM connectors + +Single Sign-On (SSO) + +Multi-region operation + +7. Important Implementation Rule + +The client specification is the source of truth for configurable workflow elements. Application code and tests should use the current specification rather than duplicating status or role values as hardcoded Python enums. + +8. Summary + +AlertDesk must provide a service for creating, viewing, assigning, and progressing security tickets. It must enforce role permissions, reject illegal status transitions, maintain an append-only audit trail, and expose OpenAPI documentation. The workflow and role rules must remain configurable through the client specification. From f0db6fd6083ab077163cfd51a53f90169396e036 Mon Sep 17 00:00:00 2001 From: Evelyn Date: Wed, 23 Sep 2026 08:43:40 -1200 Subject: [PATCH 03/17] docs: update security policy --- alertdesk/SECURITY.md | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/alertdesk/SECURITY.md b/alertdesk/SECURITY.md index f1ab565..dc1d217 100644 --- a/alertdesk/SECURITY.md +++ b/alertdesk/SECURITY.md @@ -1,29 +1,20 @@ # 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 - +## Reporting a vulnerability -[Describe your reporting process here.] +If you find a vulnerability in the system report it privately to the maintainer, don't share sensitive data publicly ## Authentication (scaffold vs Month 4) - - -[Describe auth handling here.] +Current lab uses stub tokens, proper authentication is planned for Month 4. And tokens are not used outside the lab. ## Database and audit log - - -[Describe data handling here.] +The database stores tickets, and the audit log records actions, and no sensitive secrets go in those logs ## Scope limitations - - -[Describe scope and limitations here.] +This policy applies to the alert desk internship environment. It uses stub tokens for testing only, not for production, and should not be used for real security incidents. \ No newline at end of file From 2450a2c7dd648bba168226ab92d434597f56a552 Mon Sep 17 00:00:00 2001 From: Evelyn Date: Tue, 29 Sep 2026 10:32:17 -1200 Subject: [PATCH 04/17] test: enforce analyst transition access control --- alertdesk/tests/test_auth.py | 40 +++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/alertdesk/tests/test_auth.py b/alertdesk/tests/test_auth.py index a2bf2b9..a0b8533 100644 --- a/alertdesk/tests/test_auth.py +++ b/alertdesk/tests/test_auth.py @@ -8,14 +8,48 @@ - malformed JSON / extra fields """ + +import os + import pytest +from fastapi.testclient import TestClient +from alertdesk import db +from alertdesk.app import app +@pytest.fixture() +def client(tmp_path, monkeypatch): + dbfile = tmp_path / "test.sqlite" + monkeypatch.setenv("ALERTDESK_DB", str(dbfile)) + monkeypatch.setenv( + "ALERTDESK_SPEC", + str(os.path.abspath("config/client-spec.json")) + ) -@pytest.mark.skip(reason="Week 3 — implement analyst-cannot-transition") -def test_analyst_cannot_transition(): - pass + conn = db.connect(dbfile) + db.init_schema(conn) + conn.close() + + with TestClient(app) as c: + yield c +def test_analyst_cannot_transition(client: TestClient): + created = client.post( + "/tickets", + json={"title": "Test ticket", "severity": "high"}, + headers={"Authorization": "Bearer analyst-token"}, + ) + assert created.status_code == 200 + + ticket_id = created.json()["id"] + + res = client.post( + f"/tickets/{ticket_id}/transition", + json={"status": "triaged"}, + headers={"Authorization": "Bearer analyst-token"}, + ) + assert res.status_code == 403 @pytest.mark.skip(reason="Week 3 — implement unknown bearer token") def test_unknown_token_is_401(): pass + \ No newline at end of file From ee5f5e17cfd41cf1d83eec54688fca989fe3c1dc Mon Sep 17 00:00:00 2001 From: Evelyn Date: Tue, 29 Sep 2026 14:58:09 -1200 Subject: [PATCH 05/17] test: reject unknown bearer token --- alertdesk/tests/test_auth.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/alertdesk/tests/test_auth.py b/alertdesk/tests/test_auth.py index a0b8533..f1f6bf3 100644 --- a/alertdesk/tests/test_auth.py +++ b/alertdesk/tests/test_auth.py @@ -49,7 +49,12 @@ def test_analyst_cannot_transition(client: TestClient): assert res.status_code == 403 -@pytest.mark.skip(reason="Week 3 — implement unknown bearer token") -def test_unknown_token_is_401(): - pass + +def test_unknown_token_is_401(client: TestClient): + res = client.get( + "/tickets", + headers={"Authorization": "Bearer fake-token"}, + ) + + assert res.status_code == 401 \ No newline at end of file From 21312c681b877fb08b2c3e29423c0894c801c10b Mon Sep 17 00:00:00 2001 From: Evelyn Date: Tue, 29 Sep 2026 15:16:44 -1200 Subject: [PATCH 06/17] test: verify assignment and triaged transition --- alertdesk/tests/test_auth.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/alertdesk/tests/test_auth.py b/alertdesk/tests/test_auth.py index f1f6bf3..c737d79 100644 --- a/alertdesk/tests/test_auth.py +++ b/alertdesk/tests/test_auth.py @@ -57,4 +57,31 @@ def test_unknown_token_is_401(client: TestClient): ) assert res.status_code == 401 - \ No newline at end of file + + +def test_assign_then_triaged_transition(client: TestClient): + created = client.post( + "/tickets", + json={"title": "Assigned ticket", "severity": "high"}, + headers={"Authorization": "Bearer admin-token"}, + ) + + assert created.status_code == 200 + + ticket_id = created.json()["id"] + + assigned = client.post( + f"/tickets/{ticket_id}/assign", + json={"assignee": "analyst"}, + headers={"Authorization": "Bearer admin-token"}, + ) + + assert assigned.status_code == 200 + + transitioned = client.post( + f"/tickets/{ticket_id}/transition", + json={"status": "triaged"}, + headers={"Authorization": "Bearer admin-token"}, + ) + + assert transitioned.status_code == 200 \ No newline at end of file From a40f4ded7e2a6039749541a733768945807e37a4 Mon Sep 17 00:00:00 2001 From: Evelyn Date: Tue, 29 Sep 2026 16:01:35 -1200 Subject: [PATCH 07/17] test: reject malformed JSON --- alertdesk/tests/test_auth.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/alertdesk/tests/test_auth.py b/alertdesk/tests/test_auth.py index c737d79..ab861ec 100644 --- a/alertdesk/tests/test_auth.py +++ b/alertdesk/tests/test_auth.py @@ -84,4 +84,26 @@ def test_assign_then_triaged_transition(client: TestClient): headers={"Authorization": "Bearer admin-token"}, ) - assert transitioned.status_code == 200 \ No newline at end of file + assert transitioned.status_code == 200 + +def test_invalid_severity_is_400(client: TestClient): + res = client.post( + "/tickets", + json={"title": "Invalid severity", "severity": "urgent"}, + headers={"Authorization": "Bearer admin-token"}, + ) + + assert res.status_code == 400 + + +def test_malformed_json_is_rejected(client: TestClient): + res = client.post( + "/tickets", + content='{"title": "Broken JSON", "severity": "high"', + headers={ + "Authorization": "Bearer admin-token", + "Content-Type": "application/json", + }, + ) + + assert res.status_code == 422 From 3303d5ed9d070306180d994c46ea5d8e6bcbe8d4 Mon Sep 17 00:00:00 2001 From: Evelyn Date: Thu, 1 Oct 2026 03:48:23 -1200 Subject: [PATCH 08/17] docs: fix Week 1 specification --- alertdesk/docs/SPEC.md | 44 +++++++++++++ alertdesk/docs/spec.md | 139 ----------------------------------------- 2 files changed, 44 insertions(+), 139 deletions(-) create mode 100644 alertdesk/docs/SPEC.md delete mode 100644 alertdesk/docs/spec.md diff --git a/alertdesk/docs/SPEC.md b/alertdesk/docs/SPEC.md new file mode 100644 index 0000000..16bfd54 --- /dev/null +++ b/alertdesk/docs/SPEC.md @@ -0,0 +1,44 @@ +# AlertDesk Specification + +## 1. Purpose + +AlertDesk is a web service that security analysts use to file and progress security alerts through a controlled ticket workflow. + +## 2. API Resources + +The main API resource is **tickets**. + +A ticket contains: + +- **title** +- **description** +- **severity** + +Supported ticket operations include: + +- Creating tickets +- Listing tickets +- Retrieving individual tickets +- Assigning tickets + +Authorized roles may also change ticket status. + +## 3. Severity + +Severity values are client-defined. + +The current sample values are: + +- `low` +- `medium` +- `high` +- `critical` + +The implementation must read client-defined values from configuration rather than hardcoding them in Python. + +## 4. Ticket Status Workflow + +The current sample workflow is: + +```text +new → triaged → assigned → resolved → closed \ No newline at end of file diff --git a/alertdesk/docs/spec.md b/alertdesk/docs/spec.md deleted file mode 100644 index b1af594..0000000 --- a/alertdesk/docs/spec.md +++ /dev/null @@ -1,139 +0,0 @@ -AlertDesk Specification - -1. Purpose - -AlertDesk is a web service that security analysts use to file and progress security alerts through a controlled ticket workflow. - -2. API Resources - -The main API resource is tickets. A ticket contains: - -title - -description - -severity - -Supported ticket operations include creating, listing, retrieving, and assigning tickets. Authorized roles may also change ticket status. - -3. Severity - -Severity values are client-defined. The current sample values are: - -low - -medium - -high - -critical - -The implementation must read client-defined values from configuration rather than hardcoding them in Python. - -4. Ticket Status Workflow - -The current sample statuses are: - -new -> triaged -> assigned -> resolved -> closed - -Not every transition is legal. Legal transitions must come from config/client-spec.json. The application must reject transitions that are not defined as legal. - -5. Roles and Permissions - -Role - -Create - -List/Get - -Assign - -Change Status - -Analyst - -Yes - -Yes - -Yes - -No - -Lead - -Yes - -Yes - -Yes - -Yes - -Admin - -Yes - -Yes - -Yes - -Yes - -These permissions are client-defined and must remain configuration-driven. - -6. Acceptance Criteria - -An authorized analyst can create a ticket with a title, description and valid severity. - -An authorized analyst can list tickets. - -An authorized user can retrieve an individual ticket. - -An authorized analyst can assign a ticket to an analyst identifier such as an email or handle. - -Only authorized roles can change ticket status. - -Illegal status transitions are rejected. - -Creating, assigning and transitioning a ticket append audit records. - -The running service exposes OpenAPI documentation. - -7. Audit Requirements - -The audit trail is append-only and records who performed important actions and when. At minimum, the following actions are audited: - -ticket creation - -ticket assignment - -ticket status transition - -8. Threat / Abuse Notes - -Potential abuse cases include: - -spoofed or invalid bearer tokens - -attempting actions outside the authenticated user's permissions - -attempting illegal ticket status transitions - -exposing sensitive information through audit records - -bypassing configuration-driven workflow rules through hardcoded values - -9. Out of Scope for the Scaffold - -Slack integration - -SIEM connectors - -SSO - -multi-region operation - -10. Configuration Rule - -Statuses, roles, severity values and legal transitions must be read from the client specification/configuration. If the client changes the workflow, such as adding waiting-vendor, the configuration and tests should be updated rather than introducing hardcoded status enums in Python. From ea80ad5b4c5f89f118671f770d6cd9540fdc749d Mon Sep 17 00:00:00 2001 From: Evelyn Date: Fri, 2 Oct 2026 09:20:08 -1200 Subject: [PATCH 09/17] docs: add Week 4 security assessment --- alertdesk/docs/SECURITY_ASSESSMENT.md | 85 ++++++ alertdesk/docs/openapi.json | 375 ++++++++++++++++++++++++++ 2 files changed, 460 insertions(+) create mode 100644 alertdesk/docs/SECURITY_ASSESSMENT.md create mode 100644 alertdesk/docs/openapi.json diff --git a/alertdesk/docs/SECURITY_ASSESSMENT.md b/alertdesk/docs/SECURITY_ASSESSMENT.md new file mode 100644 index 0000000..ba3b2fa --- /dev/null +++ b/alertdesk/docs/SECURITY_ASSESSMENT.md @@ -0,0 +1,85 @@ +# AlertDesk Security Assessment + + +## Summary + +AlertDesk checks bearer tokens against role mappings in `config/client-spec.json` and applies role checks to ticket actions. The tokens are fixed lab values, so they identify a configured role rather than securely verifying a real user's identity. This is suitable only for the stated lab scaffold. + +Do not deploy this authentication setup for real users or sensitive data. Before production use, replace stub tokens with an authentication system that supports identity, token expiry and revocation, and secure transport. + +## Findings by relevant OWASP Top 10:2025 categories + +### A01:2025 — Broken Access Control + +**Current safeguards:** Routes require role permissions for create, list, get, assign, and transition actions. Status changes are checked against the configured transition rules. + +**Risk:** Every role allowed to list or get tickets can access all tickets. The current code does not show per-user, team, or tenant restrictions. This may be acceptable for the lab, but would expose tickets if users should only see records within their assigned scope. + +**Next step:** Define the intended ticket visibility rules and enforce them on list, get, assign, and transition operations before adding real users. + +### A02:2025 — Security Misconfiguration + +**Current safeguards:** The project describes its tokens as lab stubs, and the API documents its purpose. + +**Risk:** OpenAPI and Swagger documentation are enabled by default. The sample tokens are stored in the repository's client specification. This is convenient for the lab but should not be treated as safe production configuration. + +**Next step:** For any deployment with real data, review documentation exposure and move credentials to an appropriate secret-management mechanism. + +### A03:2025 — Software Supply Chain Failures + +**Observation:** The project specifies minimum dependency versions in `pyproject.toml`. This assessment did not verify a lock file, dependency scanning, or the integrity of installed packages. + +**Next step:** Add dependency review and update checks to the project workflow before production use. + +### A04:2025 — Cryptographic Failures + +**Risk:** Bearer tokens are credentials. Anyone who obtains a token can use its associated role. The application code does not configure transport encryption; HTTPS must be provided by the deployment environment. + +**Next step:** Require HTTPS wherever tokens are sent, and replace the fixed lab tokens with credentials that can be rotated and revoked. + +### A05:2025 — Injection + +**Current safeguard:** Database operations shown in the application use SQL parameters for user-controlled values, which reduces SQL injection risk in those queries. + +**Limit:** This is a code review of the visible queries, not a complete security test. + +**Next step:** Keep using parameterized queries for all database operations. + +### A06:2025 — Insecure Design + +**Risk:** The authentication design intentionally maps static shared tokens directly to roles. It does not provide individual user identity, token expiry, or revocation. This limits accountability and incident response. + +**Next step:** Treat the scaffold as lab-only and design production identity, authorization, and credential lifecycle requirements before expanding deployment. + +### A07:2025 — Authentication Failures + +**Risk:** The tokens in `config/client-spec.json` are fixed sample values. The code checks whether a supplied token appears in that file, but the tokens do not expire or have a revocation mechanism. The HTTP Bearer scheme documents how clients send credentials; it does not make the tokens cryptographically secure. + +**Next step:** Do not reuse the sample tokens outside the lab. Replace them with an authentication system that supports individual identities, expiration, rotation, and revocation. + +### A09:2025 — Security Logging and Alerting Failures + +**Current safeguards:** Ticket creation, assignment, and status transitions write audit rows. + +**Risks:** Audit rows record the actor's role rather than a distinct user identity. Event details include ticket titles, assignees, and status changes, which may contain sensitive information. The table schema does not enforce append-only behavior, and this review did not verify log access controls, retention, or alerting. + +**Next step:** Define what information may be logged, minimize sensitive details, and add appropriate access, retention, and integrity controls before production use. + +### A10:2025 — Mishandling of Exceptional Conditions + +**Current safeguards:** The application returns explicit responses for several expected conditions, including missing or unknown tokens, insufficient role permissions, missing tickets, invalid severities, and illegal status transitions. + +**Limit:** This assessment did not evaluate every unexpected database or runtime failure. + +**Next step:** Review error handling for unexpected failures and ensure responses do not expose internal details. + +## Scope and limitations + +This assessment covers the current code scaffold and its documented lab configuration. It does not verify deployment settings, HTTPS termination, host security, dependency integrity, or operational monitoring. AlertDesk is an internship project scaffold, not a production security platform. + +## OWASP reference + +- [OWASP Top 10:2025](https://top10.owasp.org/2025/) +- [A01:2025 — Broken Access Control](https://top10.owasp.org/2025/A01_2025-Broken_Access_Control/) +- [A07:2025 — Authentication Failures](https://top10.owasp.org/2025/A07_2025-Authentication_Failures/) +- [A09:2025 — Security Logging and Alerting Failures](https://top10.owasp.org/2025/A09_2025-Security_Logging_and_Alerting_Failures/) \ No newline at end of file diff --git a/alertdesk/docs/openapi.json b/alertdesk/docs/openapi.json new file mode 100644 index 0000000..b22d2a1 --- /dev/null +++ b/alertdesk/docs/openapi.json @@ -0,0 +1,375 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "AlertDesk", + "description": "Security alert and ticket intake. Statuses and roles come from client-spec.json.", + "version": "0.1.0" + }, + "paths": { + "/health": { + "get": { + "summary": "Health", + "operationId": "health_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Response Health Health Get" + } + } + } + } + } + } + }, + "/tickets": { + "get": { + "summary": "List Tickets", + "operationId": "list_tickets_tickets_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Response List Tickets Tickets Get" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + }, + "post": { + "summary": "Create Ticket", + "operationId": "create_ticket_tickets_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TicketCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "additionalProperties": true, + "type": "object", + "title": "Response Create Ticket Tickets Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + } + ] + } + }, + "/tickets/{ticket_id}": { + "get": { + "summary": "Get Ticket", + "operationId": "get_ticket_tickets__ticket_id__get", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "ticket_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Ticket Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Get Ticket Tickets Ticket Id Get" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/tickets/{ticket_id}/assign": { + "post": { + "summary": "Assign Ticket", + "operationId": "assign_ticket_tickets__ticket_id__assign_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "ticket_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Ticket Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssignBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Assign Ticket Tickets Ticket Id Assign Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/tickets/{ticket_id}/transition": { + "post": { + "summary": "Transition Ticket", + "operationId": "transition_ticket_tickets__ticket_id__transition_post", + "security": [ + { + "HTTPBearer": [] + } + ], + "parameters": [ + { + "name": "ticket_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Ticket Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TransitionBody" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true, + "title": "Response Transition Ticket Tickets Ticket Id Transition Post" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "AssignBody": { + "properties": { + "assignee": { + "type": "string", + "minLength": 1, + "title": "Assignee" + } + }, + "type": "object", + "required": [ + "assignee" + ], + "title": "AssignBody" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "TicketCreate": { + "properties": { + "title": { + "type": "string", + "minLength": 1, + "title": "Title" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "severity": { + "type": "string", + "title": "Severity" + } + }, + "type": "object", + "required": [ + "title", + "severity" + ], + "title": "TicketCreate" + }, + "TransitionBody": { + "properties": { + "status": { + "type": "string", + "title": "Status" + } + }, + "type": "object", + "required": [ + "status" + ], + "title": "TransitionBody" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + }, + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer" + } + } + } +} From 34bc4ac05440f78cb51dbc911acd5c9f7bdca561 Mon Sep 17 00:00:00 2001 From: Evelyn Date: Sun, 4 Oct 2026 01:46:23 -1200 Subject: [PATCH 10/17] docs: update security policy --- alertdesk/SECURITY.md | 51 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/alertdesk/SECURITY.md b/alertdesk/SECURITY.md index dc1d217..9e2a87c 100644 --- a/alertdesk/SECURITY.md +++ b/alertdesk/SECURITY.md @@ -1,20 +1,61 @@ # Security Policy +## Reporting a vulnerability +If you find a vulnerability in AlertDesk, report it privately to the +maintainer: -## Reporting a vulnerability +- **Name:** George +- **Email:** baningeorge@gmail.com -If you find a vulnerability in the system report it privately to the maintainer, don't share sensitive data publicly +Do not publicly disclose vulnerabilities or include sensitive information +in public issues. ## Authentication (scaffold vs Month 4) +The current lab uses stub bearer tokens for authentication. + +The lab tokens and their associated roles are defined in: + +`config/client-spec.json` -Current lab uses stub tokens, proper authentication is planned for Month 4. And tokens are not used outside the lab. +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 -The database stores tickets, and the audit log records actions, and no sensitive secrets go in those logs +AlertDesk stores ticket data in the configured SQLite database. + +The database location is controlled by the `ALERTDESK_DB` environment +variable. + +Security-relevant actions are recorded in the append-only +`audit_events` table. At minimum, ticket creation, assignment, and +status transitions are audited. + +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 -This policy applies to the alert desk internship environment. It uses stub tokens for testing only, not for production, and should not be used for real security incidents. \ No newline at end of file +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: + +- Slack integration +- SIEM connectors +- SSO +- Multi-region operation \ No newline at end of file From e5f8f22a15bf1bb66aa432bb62ef92ea61eae0aa Mon Sep 17 00:00:00 2001 From: Evelyn Date: Mon, 5 Oct 2026 11:04:16 -1200 Subject: [PATCH 11/17] feat: add Week 5 observability and benchmarks --- alertdesk/alertdesk/app.py | 5 ++ alertdesk/alertdesk/observability.py | 58 ++++++++++++++++++ alertdesk/docs/BENCHMARKS.md | 44 +++++++++++++ alertdesk/scripts/benchmark.py | 92 ++++++++++++++++++++++++++++ 4 files changed, 199 insertions(+) create mode 100644 alertdesk/alertdesk/observability.py create mode 100644 alertdesk/docs/BENCHMARKS.md create mode 100644 alertdesk/scripts/benchmark.py diff --git a/alertdesk/alertdesk/app.py b/alertdesk/alertdesk/app.py index 03ea4d0..6d18b2c 100644 --- a/alertdesk/alertdesk/app.py +++ b/alertdesk/alertdesk/app.py @@ -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): @@ -28,6 +31,8 @@ async def lifespan(_app: FastAPI): lifespan=lifespan, ) +app.add_middleware(RequestLoggingMiddleware) + class TicketCreate(BaseModel): title: str = Field(min_length=1) diff --git a/alertdesk/alertdesk/observability.py b/alertdesk/alertdesk/observability.py new file mode 100644 index 0000000..d4dd9b4 --- /dev/null +++ b/alertdesk/alertdesk/observability.py @@ -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, + }}, + ) diff --git a/alertdesk/docs/BENCHMARKS.md b/alertdesk/docs/BENCHMARKS.md new file mode 100644 index 0000000..d754bef --- /dev/null +++ b/alertdesk/docs/BENCHMARKS.md @@ -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. diff --git a/alertdesk/scripts/benchmark.py b/alertdesk/scripts/benchmark.py new file mode 100644 index 0000000..db0ad28 --- /dev/null +++ b/alertdesk/scripts/benchmark.py @@ -0,0 +1,92 @@ +"""Measure AlertDesk create and list request latency against a running API. + +Example: python scripts/benchmark.py --base-url http://127.0.0.1:8000 --iterations 100 +The service must already be running. Each run creates uniquely named tickets. +""" + +from __future__ import annotations + +import argparse +import json +import math +import statistics +import time +import urllib.error +import urllib.request +import uuid + + +def request(base_url: str, path: str, token: str, payload: dict | None = None) -> tuple[int, float]: + data = json.dumps(payload).encode() if payload is not None else None + req = urllib.request.Request( + f"{base_url.rstrip('/')}{path}", data=data, + headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}, + method="POST" if data is not None else "GET", + ) + started = time.perf_counter() + try: + with urllib.request.urlopen(req, timeout=30) as response: + response.read() + status = response.status + except urllib.error.HTTPError as exc: + exc.read() + status = exc.code + elapsed_ms = (time.perf_counter() - started) * 1000 + if not 200 <= status < 300: + raise RuntimeError(f"{req.method} {path} returned HTTP {status}") + return status, elapsed_ms + + +def percentile(values: list[float], percentile_value: float) -> float: + ordered = sorted(values) + index = max(0, math.ceil(percentile_value * len(ordered)) - 1) + return ordered[index] + + +def summarize(values: list[float]) -> dict[str, float]: + return { + "count": len(values), + "mean_ms": round(statistics.mean(values), 3), + "median_ms": round(statistics.median(values), 3), + "p95_ms": round(percentile(values, 0.95), 3), + "min_ms": round(min(values), 3), + "max_ms": round(max(values), 3), + } + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--base-url", default="http://127.0.0.1:8000") + parser.add_argument("--token", default="analyst-token") + parser.add_argument("--iterations", type=int, default=100) + parser.add_argument("--warmup", type=int, default=5) + args = parser.parse_args() + if args.iterations < 1 or args.warmup < 0: + parser.error("--iterations must be positive and --warmup cannot be negative") + + create_times: list[float] = [] + list_times: list[float] = [] + for index in range(args.warmup + args.iterations): + payload = { + "title": f"benchmark-{uuid.uuid4().hex}", + "description": "Synthetic benchmark record", + "severity": "low", + } + _, create_ms = request(args.base_url, "/tickets", args.token, payload) + _, list_ms = request(args.base_url, "/tickets", args.token) + if index >= args.warmup: + create_times.append(create_ms) + list_times.append(list_ms) + + print(json.dumps({ + "base_url": args.base_url.rstrip("/"), + "iterations": args.iterations, + "warmup": args.warmup, + "units": "ms", + "create_ticket": summarize(create_times), + "list_tickets": summarize(list_times), + }, indent=2)) + + +if __name__ == "__main__": + main() From df5a34bbcc15cdee41ebc50fe6d3b45be994331f Mon Sep 17 00:00:00 2001 From: Evelyn Date: Wed, 7 Oct 2026 11:18:33 -1200 Subject: [PATCH 12/17] Week 7: add container deployment and scan setup --- alertdesk/.dockerignore | 14 ++++++++++++++ alertdesk/Dockerfile | 21 +++++++++++++++++++++ alertdesk/README.md | 7 +++++++ alertdesk/compose.yaml | 12 ++++++++++++ alertdesk/docs/DEPLOYMENT.md | 27 +++++++++++++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 alertdesk/.dockerignore create mode 100644 alertdesk/Dockerfile create mode 100644 alertdesk/compose.yaml create mode 100644 alertdesk/docs/DEPLOYMENT.md diff --git a/alertdesk/.dockerignore b/alertdesk/.dockerignore new file mode 100644 index 0000000..2343052 --- /dev/null +++ b/alertdesk/.dockerignore @@ -0,0 +1,14 @@ +.git +.venv +venv +__pycache__ +*.py[cod] +*.egg-info +.pytest_cache +.coverage +htmlcov +tests +data/* +!data/.gitkeep +*.db +*.sqlite diff --git a/alertdesk/Dockerfile b/alertdesk/Dockerfile new file mode 100644 index 0000000..fdcfcd4 --- /dev/null +++ b/alertdesk/Dockerfile @@ -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"] diff --git a/alertdesk/README.md b/alertdesk/README.md index 8156c6d..c157d0a 100644 --- a/alertdesk/README.md +++ b/alertdesk/README.md @@ -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 | diff --git a/alertdesk/compose.yaml b/alertdesk/compose.yaml new file mode 100644 index 0000000..cd0e324 --- /dev/null +++ b/alertdesk/compose.yaml @@ -0,0 +1,12 @@ +services: + alertdesk: + build: + context: . + ports: + - "8000:8000" + volumes: + - alertdesk-data:/app/data + restart: unless-stopped + +volumes: + alertdesk-data: diff --git a/alertdesk/docs/DEPLOYMENT.md b/alertdesk/docs/DEPLOYMENT.md new file mode 100644 index 0000000..e8db6ff --- /dev/null +++ b/alertdesk/docs/DEPLOYMENT.md @@ -0,0 +1,27 @@ +# Local container deployment + +From the `alertdesk/` directory, start the API with: + +```bash +docker compose up --build +``` + +The API is available at . 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. + +The CI `container-scan` job builds this Dockerfile and fails on Trivy HIGH or +CRITICAL findings. From 8f2f572bc081257a6e80e8375b010d9e6b1407c1 Mon Sep 17 00:00:00 2001 From: Evelyn Date: Wed, 14 Oct 2026 13:52:45 -1200 Subject: [PATCH 13/17] fix: address Week 7 review feedback --- alertdesk/SECURITY.md | 8 +++++--- alertdesk/alertdesk/db.py | 12 ++++++++++++ alertdesk/docs/DEPLOYMENT.md | 8 ++++++++ alertdesk/docs/SPEC.md | 13 ++++++++++--- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/alertdesk/SECURITY.md b/alertdesk/SECURITY.md index 9e2a87c..a07e3e1 100644 --- a/alertdesk/SECURITY.md +++ b/alertdesk/SECURITY.md @@ -31,9 +31,11 @@ AlertDesk stores ticket data in the configured SQLite database. The database location is controlled by the `ALERTDESK_DB` environment variable. -Security-relevant actions are recorded in the append-only -`audit_events` table. At minimum, ticket creation, assignment, and -status transitions are audited. +Security-relevant actions are recorded in the `audit_events` table. Ticket +creation, assignment, and status transitions are audited. SQLite +`BEFORE UPDATE` and `BEFORE DELETE` triggers abort attempts to change or remove +audit rows, enforcing append-only behavior through normal database +operations. Audit logs must not contain passwords, bearer tokens, or other sensitive secrets. diff --git a/alertdesk/alertdesk/db.py b/alertdesk/alertdesk/db.py index fbabfa2..7420a34 100644 --- a/alertdesk/alertdesk/db.py +++ b/alertdesk/alertdesk/db.py @@ -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() diff --git a/alertdesk/docs/DEPLOYMENT.md b/alertdesk/docs/DEPLOYMENT.md index e8db6ff..dbc21e6 100644 --- a/alertdesk/docs/DEPLOYMENT.md +++ b/alertdesk/docs/DEPLOYMENT.md @@ -23,5 +23,13 @@ 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 output 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 CRITICAL findings. diff --git a/alertdesk/docs/SPEC.md b/alertdesk/docs/SPEC.md index 16bfd54..09d94bd 100644 --- a/alertdesk/docs/SPEC.md +++ b/alertdesk/docs/SPEC.md @@ -38,7 +38,14 @@ The implementation must read client-defined values from configuration rather tha ## 4. Ticket Status Workflow -The current sample workflow is: +The configured statuses are `new`, `triaged`, `assigned`, `resolved`, and +`closed`. Legal transitions, as defined in `config/client-spec.json`, are: -```text -new → triaged → assigned → resolved → closed \ No newline at end of file +- `new` -> `triaged` or `closed` +- `triaged` -> `assigned` or `closed` +- `assigned` -> `resolved` or `closed` +- `resolved` -> `closed` +- `closed` -> no further status + +The service reads this transition map from the client specification and rejects +status changes not listed there. From 1d98704ae0a740019edf77358a1a275b820f730d Mon Sep 17 00:00:00 2001 From: Evelyn Date: Wed, 28 Oct 2026 12:31:35 -1200 Subject: [PATCH 14/17] fix: include interns branches in AlertDesk workflow --- .github/workflows/alertdesk.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/alertdesk.yml b/.github/workflows/alertdesk.yml index f603784..4f9db51 100644 --- a/.github/workflows/alertdesk.yml +++ b/.github/workflows/alertdesk.yml @@ -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" From 78426185f2f5b90b6304d5a776ae79e1b5531dca Mon Sep 17 00:00:00 2001 From: Evelyn Date: Mon, 2 Nov 2026 01:20:37 -1200 Subject: [PATCH 15/17] fix: remove unsupported hashFiles workflow condition --- .github/workflows/alertdesk.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/alertdesk.yml b/.github/workflows/alertdesk.yml index 4f9db51..73015c9 100644 --- a/.github/workflows/alertdesk.yml +++ b/.github/workflows/alertdesk.yml @@ -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 . From a72029f37d97eedc425b46b8c5fc1577c6b8b358 Mon Sep 17 00:00:00 2001 From: Evelyn Date: Mon, 2 Nov 2026 02:09:50 -1200 Subject: [PATCH 16/17] docs: update AlertDesk functional requirements --- alertdesk/docs/functional_requirements | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/alertdesk/docs/functional_requirements b/alertdesk/docs/functional_requirements index a41767f..fdc0600 100644 --- a/alertdesk/docs/functional_requirements +++ b/alertdesk/docs/functional_requirements @@ -72,19 +72,19 @@ FR-11 Audit ticket creation -The system shall record ticket-creation actions in an append-only audit trail. +The system shall record ticket-creation actions in an audit trail. FR-12 Audit assignment -The system shall record ticket-assignment actions in an append-only audit trail. +The system shall record ticket-assignment actions in an audit trail. FR-13 Audit status transition -The system shall record ticket-status transitions in an append-only audit trail. +The system shall record ticket-status transitions in an audit trail. FR-14 @@ -114,13 +114,21 @@ Lead Yes Yes Yes Yes Admin Yes Yes Yes Yes -4. Current Sample Workflow +4. Current Sample Workflow -The current sample workflow is: +The current sample workflow is defined by `config/client-spec.json`. + +Legal transitions are: + +- `new` → `triaged` or `closed` +- `triaged` → `assigned` or `closed` +- `assigned` → `resolved` or `closed` +- `resolved` → `closed` +- `closed` → no further status + +The application shall read the legal transitions from the JSON specification and reject status changes that are not permitted. Direct transitions to `closed` are allowed where defined by the client specification. -New → Triaged → Assigned → Resolved → Closed -The application shall not allow arbitrary status jumps. The legal transitions must be read from the JSON specification. 5. Acceptance Criteria @@ -150,7 +158,7 @@ An attempted illegal status transition is rejected. Audit trail -Creating, assigning, or transitioning a ticket creates an append-only audit record identifying the actor and time. +Creating, assigning, or transitioning a ticket creates an audit record identifying the actor and time. Configuration @@ -176,4 +184,4 @@ The client specification is the source of truth for configurable workflow elemen 8. Summary -AlertDesk must provide a service for creating, viewing, assigning, and progressing security tickets. It must enforce role permissions, reject illegal status transitions, maintain an append-only audit trail, and expose OpenAPI documentation. The workflow and role rules must remain configurable through the client specification. +AlertDesk must provide a service for creating, viewing, assigning, and progressing security tickets. It must enforce role permissions, reject illegal status transitions, maintain an audit trail, and expose OpenAPI documentation. The workflow and role rules must remain configurable through the client specification. \ No newline at end of file From 12dc59d6eadab5883f97e748c96ec12bba1f1627 Mon Sep 17 00:00:00 2001 From: Evelyn Date: Wed, 4 Nov 2026 01:08:30 -1200 Subject: [PATCH 17/17] fix: address Week 7 security and deployment feedback --- alertdesk/SECURITY.md | 11 +++++----- alertdesk/compose.yaml | 2 +- alertdesk/docs/DEPLOYMENT.md | 6 +++--- alertdesk/tests/test_tickets.py | 38 ++++++++++++++++++++++++++++++++- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/alertdesk/SECURITY.md b/alertdesk/SECURITY.md index a07e3e1..7411989 100644 --- a/alertdesk/SECURITY.md +++ b/alertdesk/SECURITY.md @@ -32,10 +32,11 @@ The database location is controlled by the `ALERTDESK_DB` environment variable. Security-relevant actions are recorded in the `audit_events` table. Ticket -creation, assignment, and status transitions are audited. SQLite -`BEFORE UPDATE` and `BEFORE DELETE` triggers abort attempts to change or remove -audit rows, enforcing append-only behavior through normal database -operations. +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. @@ -60,4 +61,4 @@ The following are outside the current scaffold scope: - Slack integration - SIEM connectors - SSO -- Multi-region operation \ No newline at end of file +- Multi-region operation \ No newline at end of file diff --git a/alertdesk/compose.yaml b/alertdesk/compose.yaml index cd0e324..fc9578f 100644 --- a/alertdesk/compose.yaml +++ b/alertdesk/compose.yaml @@ -3,7 +3,7 @@ services: build: context: . ports: - - "8000:8000" + - "127.0.0.1:8001:8000" volumes: - alertdesk-data:/app/data restart: unless-stopped diff --git a/alertdesk/docs/DEPLOYMENT.md b/alertdesk/docs/DEPLOYMENT.md index dbc21e6..60b5d7d 100644 --- a/alertdesk/docs/DEPLOYMENT.md +++ b/alertdesk/docs/DEPLOYMENT.md @@ -27,9 +27,9 @@ fresh database and a copy of the prior release's database. 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 output and can be collected by the -container runtime's logging system. +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 CRITICAL findings. diff --git a/alertdesk/tests/test_tickets.py b/alertdesk/tests/test_tickets.py index 4c35ca5..499c938 100644 --- a/alertdesk/tests/test_tickets.py +++ b/alertdesk/tests/test_tickets.py @@ -1,8 +1,9 @@ -"""Starter API tests — one happy path and one illegal transition.""" +"""Starter API tests — ticket creation, transitions, and audit protection.""" from __future__ import annotations import os +import sqlite3 import pytest from fastapi.testclient import TestClient @@ -19,6 +20,7 @@ def client(tmp_path, monkeypatch): conn = db.connect(dbfile) db.init_schema(conn) conn.close() + with TestClient(app) as c: yield c @@ -48,3 +50,37 @@ def test_illegal_transition_new_to_resolved(client: TestClient): headers={"Authorization": "Bearer lead-token"}, ) assert res.status_code == 409 + + +def test_audit_events_cannot_be_updated(client: TestClient): + conn = db.connect() + conn.execute( + "INSERT INTO audit_events (actor, action, detail) VALUES (?, ?, ?)", + ("admin", "test", "original detail"), + ) + conn.commit() + + with pytest.raises(sqlite3.IntegrityError): + conn.execute( + "UPDATE audit_events SET detail = ? WHERE action = ?", + ("modified detail", "test"), + ) + + conn.close() + + +def test_audit_events_cannot_be_deleted(client: TestClient): + conn = db.connect() + conn.execute( + "INSERT INTO audit_events (actor, action, detail) VALUES (?, ?, ?)", + ("admin", "test", "original detail"), + ) + conn.commit() + + with pytest.raises(sqlite3.IntegrityError): + conn.execute( + "DELETE FROM audit_events WHERE action = ?", + ("test",), + ) + + conn.close()