Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions backend/was/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ __pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.venv/
.env
local-output/
was_config.txt
11 changes: 11 additions & 0 deletions backend/was/Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
PYTHON ?= ../../was_report/bin/python
REPO_ROOT ?= ../..
ROOT_PYTHON ?= ./was_report/bin/python
DJANGO_TEST_LABELS ?= accounts audit core operations qualys_admin reports stakeholders tracker
IMAGE ?= was-reporting
ENV_FILE ?= .env
OUTPUT_DIR ?= $(CURDIR)/local-output
Expand Down Expand Up @@ -61,8 +62,10 @@ HOST_SOFTWARE_INVENTORY_DIR ?= $(OUTPUT_DIR)/host-inventory-$(shell date -u +%Y%
HOST_SOFTWARE_SINCE ?=
HOST_SOFTWARE_BASELINE ?=


.PHONY: install test lint build menu help report-help xml-help inventory admin-help mailer-help stakeholders-help special-cases stakeholder-export stakeholder-import tracker-csv tracker-import tracker-table tracker-mark-sent report-errors update-tracker update-tracker-delete-apps assignee-digests recent-scan-batch recent-scan-batch-assignee-test recent-scan-batch-test single-report manual-report recover-manual-reports recover-interrupted-batch reconcile-email-delivery logs-latest logs-summary logs-errors logs-tag host-software-capture host-software-preview host-software-apply host-software-verify host-shell-preview host-shell-install host-operator-scripts-preview host-operator-scripts-install


export APPLY BATCH_DAYS_BACK BATCH_RUN_ID BATCH_TEST_RECIPIENTS BATCH_WORKERS
export CAPACITY_ACTION CAPACITY_CONTINUE_RUN_ID CAPACITY_EXPECTED_CANDIDATES
export CAPACITY_RUN_ID CAPACITY_WORKLOAD_LABEL ENV_FILE IMAGE OUTPUT_DIR PYTHON
Expand All @@ -81,6 +84,14 @@ install:

test:
cd $(REPO_ROOT) && PYTHONPATH=backend/was/src $(ROOT_PYTHON) -m unittest discover -s backend/was/tests
$(MAKE) django-test

django-test:
@cd $(REPO_ROOT) && \
DJANGO_KEY="$$($(ROOT_PYTHON) -c 'import secrets; print(secrets.token_urlsafe(50))')" \
IS_LOCAL=true \
PYTHONPATH=backend/was/src \
$(ROOT_PYTHON) backend/was/src/was_django_project/manage.py test $(DJANGO_TEST_LABELS)

lint:
cd $(REPO_ROOT) && $(ROOT_PYTHON) -m flake8 \
Expand Down
47 changes: 46 additions & 1 deletion backend/was/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,51 @@ test Make commands, troubleshooting, updates, and rollback guidance.
Update the relevant runbooks in the same review as any behavior, command,
schema, configuration, safeguard, or side-effect change.

## Django Development Setup

The Django project is located at `src/was_django_project`. First create the
ignored local environment file from the tracked placeholder inventory:

```bash
cd backend/was
./scripts/create-local-env.sh
```

Replace every placeholder in `.env` with an approved local-development value,
use a unique `DJANGO_KEY`, retain `IS_LOCAL=true`, and never commit `.env` or
populated credentials. Then create and activate a local uv-managed virtual
environment, install the WAS requirements and package, load `.env`, and start
the development server:

```bash
cd backend/was
uv venv --python 3.12 .venv
source .venv/bin/activate
uv pip install -r requirements.txt
uv pip install --editable .
uv run --env-file .env python src/was_django_project/manage.py check
uv run --env-file .env python src/was_django_project/manage.py runserver \
127.0.0.1:8000
```

The Django settings require access to an approved development PostgreSQL
database through the `WAS_DB_*` values in `.env`. Do not use production database
credentials for local development. Network access to the configured database
must be established before running migrations, database-backed tests, or pages
that query application data.

Run the Django smoke tests from `backend/was` with the virtual environment
active:

```bash
uv run --env-file .env python src/was_django_project/manage.py test \
accounts audit core operations qualys_admin reports stakeholders tracker
```

The initial smoke tests do not access PostgreSQL. Future model and integration
tests may create and remove the configured test database, so confirm that the
test credentials and database name are isolated before running them.

For isolated load testing and production timing metrics, see
[Capacity testing](docs/capacity-testing.md). The target is 600 report outcomes
within eight hours through persisted SES acceptance, with no operational tracker
Expand Down Expand Up @@ -2023,7 +2068,7 @@ targets.
| Target | Required input and effect |
| --- | --- |
| `make install` | Installs the host Python requirements and editable package. |
| `make test` | Runs the WAS unit-test suite. |
| `make test` | Runs the WAS and Django unit-test suites. |
| `make lint` | Runs the repository's WAS flake8 command. |
| `make build` | Rebuilds the local `was-reporting` image. |
| `make menu` | Opens the interactive menu with live environment access. Menu choices can write data or send email after confirmation. |
Expand Down
10 changes: 10 additions & 0 deletions backend/was/dev.env
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,20 @@ WAS_PASSWORD_LENGTH=24

# AWS SDK configuration
AWS_DEFAULT_REGION=us-east-1
AWS_REGION=us-east-1
STAGE=dev

# Email delivery
WAS_EMAIL_SOURCE=verified-sender@example.gov
WAS_SES_ROLE_ARN=arn:aws:iam::246048611598:role/SesSendEmail-cyber.dhs.gov

WAS_QUALYS_REPORT_XML_MAX_BYTES=10737418240
WAS_QUALYS_REPORT_XML_MIN_FREE_BYTES=5368709120

# Django settings
DJANGO_KEY='<replace-me>'
IS_LOCAL=true
ALLOWED_HOSTS=localhost,127.0.0.1,web

# AWS supplies AWS_LAMBDA_FUNCTION_NAME or ECS_CONTAINER_METADATA_URI at runtime.
# Do not define either variable for local development.
3 changes: 3 additions & 0 deletions backend/was/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
boto3==1.43.88
botocore==1.43.88
defusedxml==0.7.1
Django==5.2.18
lxml==6.1.2
mangum==0.22.0
matplotlib==3.11.1
numpy==2.5.2
openpyxl==3.1.5
Expand All @@ -18,3 +20,4 @@ python-dateutil==2.9.0.post0
qualysapi==5.1.0
requests==2.34.2
seaborn==0.13.2
watchtower==3.4.0
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/accounts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Accounts application package."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/accounts/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Django admin configuration for the accounts application."""
11 changes: 11 additions & 0 deletions backend/was/src/was_django_project/accounts/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Django application configuration for accounts."""

# Third-Party Libraries
from django.apps import AppConfig


class AccountsConfig(AppConfig):
"""Configure the accounts Django application."""

default_auto_field = "django.db.models.BigAutoField"
name = "accounts"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Accounts database migrations."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/accounts/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Database models for the accounts application."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/accounts/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the accounts application."""
8 changes: 8 additions & 0 deletions backend/was/src/was_django_project/accounts/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""URL configuration for the accounts application."""

# Third-Party Libraries
from django.urls.resolvers import URLPattern, URLResolver

app_name = "accounts"

urlpatterns: list[URLPattern | URLResolver] = []
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/accounts/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Views for the accounts application."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/audit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Audit application package."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/audit/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Django admin configuration for the audit application."""
11 changes: 11 additions & 0 deletions backend/was/src/was_django_project/audit/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Django application configuration for audit."""

# Third-Party Libraries
from django.apps import AppConfig


class AuditConfig(AppConfig):
"""Configure the audit Django application."""

default_auto_field = "django.db.models.BigAutoField"
name = "audit"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Audit database migrations."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/audit/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Database models for the audit application."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/audit/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the audit application."""
8 changes: 8 additions & 0 deletions backend/was/src/was_django_project/audit/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""URL configuration for the audit application."""

# Third-Party Libraries
from django.urls.resolvers import URLPattern, URLResolver

app_name = "audit"

urlpatterns: list[URLPattern | URLResolver] = []
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/audit/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Views for the audit application."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Core application package."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/core/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Django admin configuration for the core application."""
11 changes: 11 additions & 0 deletions backend/was/src/was_django_project/core/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Django application configuration for core."""

# Third-Party Libraries
from django.apps import AppConfig


class CoreConfig(AppConfig):
"""Configure the core Django application."""

default_auto_field = "django.db.models.BigAutoField"
name = "core"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Core database migrations."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/core/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Database models for the core application."""
62 changes: 62 additions & 0 deletions backend/was/src/was_django_project/core/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Smoke tests for the WAS Django application foundation."""

# Third-Party Libraries
from django.contrib.messages import constants as message_constants
from django.contrib.messages.storage.base import Message
from django.template.loader import render_to_string
from django.test import SimpleTestCase
from django.urls import resolve, reverse
from mangum import Mangum

# Local Application
from was_django.asgi import application, handler


class DjangoFoundationSmokeTests(SimpleTestCase):
"""Verify the initial pages and serverless application entry point."""

def test_landing_page_response(self) -> None:
"""Return the landing page through the named root route."""
response = self.client.get(reverse("core:home"))

self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "core/home.html")
self.assertContains(response, "WAS Reporting")

def test_health_endpoint_response(self) -> None:
"""Return only the expected status from the health endpoint."""
response = self.client.get(reverse("core:health"))

self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"status": "ok"})

def test_base_template_renders_shared_landmarks(self) -> None:
"""Render the shared shell with its skip link and main landmark."""
rendered_template = render_to_string(
"core/home.html",
{
"DEFAULT_MESSAGE_LEVELS": message_constants.DEFAULT_LEVELS,
"messages": [Message(message_constants.ERROR, "Example error message")],
"page_title": "Foundation smoke test",
},
)

self.assertIn("<title>", rendered_template)
self.assertIn("Foundation smoke test", rendered_template)
self.assertIn("css/was.css", rendered_template)
self.assertIn('href="#main-content"', rendered_template)
self.assertIn('id="main-content"', rendered_template)
self.assertIn('aria-label="Primary navigation"', rendered_template)
self.assertIn("Error:", rendered_template)
self.assertIn("Example error message", rendered_template)

def test_mangum_handler_wraps_asgi_application(self) -> None:
"""Expose the Django ASGI application through Mangum for Lambda."""
self.assertIsInstance(handler, Mangum)
self.assertIs(handler.app, application)

def test_root_url_resolves_to_named_home_view(self) -> None:
"""Load the project URL configuration and resolve its root route."""
resolved_url = resolve("/")

self.assertEqual(resolved_url.view_name, "core:home")
15 changes: 15 additions & 0 deletions backend/was/src/was_django_project/core/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""URL configuration for the core application."""

# Third-Party Libraries
from django.urls import path
from django.urls.resolvers import URLPattern, URLResolver

# Local Application
from . import views

app_name = "core"

urlpatterns: list[URLPattern | URLResolver] = [
path("", views.home, name="home"),
path("health/", views.health, name="health"),
]
15 changes: 15 additions & 0 deletions backend/was/src/was_django_project/core/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Views for the core application."""

# Third-Party Libraries
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import render


def home(request: HttpRequest) -> HttpResponse:
"""Render the initial WAS Reporting application landing page."""
return render(request, "core/home.html", {"page_title": "WAS Reporting"})


def health(request: HttpRequest) -> JsonResponse:
"""Return a minimal response for local and AWS health checks."""
return JsonResponse({"status": "ok"})
24 changes: 24 additions & 0 deletions backend/was/src/was_django_project/manage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
# Standard Python Libraries
import os
import sys


def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "was_django.settings")
try:
# Third-Party Libraries
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Operations application package."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/operations/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Django admin configuration for the operations application."""
11 changes: 11 additions & 0 deletions backend/was/src/was_django_project/operations/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Django application configuration for operations."""

# Third-Party Libraries
from django.apps import AppConfig


class OperationsConfig(AppConfig):
"""Configure the operations Django application."""

default_auto_field = "django.db.models.BigAutoField"
name = "operations"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Operations database migrations."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/operations/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Database models for the operations application."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/operations/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the operations application."""
8 changes: 8 additions & 0 deletions backend/was/src/was_django_project/operations/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""URL configuration for the operations application."""

# Third-Party Libraries
from django.urls.resolvers import URLPattern, URLResolver

app_name = "operations"

urlpatterns: list[URLPattern | URLResolver] = []
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/operations/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Views for the operations application."""
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Qualys administration application package."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/qualys_admin/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Django admin configuration for the Qualys administration application."""
11 changes: 11 additions & 0 deletions backend/was/src/was_django_project/qualys_admin/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Django application configuration for Qualys administration."""

# Third-Party Libraries
from django.apps import AppConfig


class QualysAdminConfig(AppConfig):
"""Configure the Qualys administration Django application."""

default_auto_field = "django.db.models.BigAutoField"
name = "qualys_admin"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Qualys administration database migrations."""
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Database models for the Qualys administration application."""
1 change: 1 addition & 0 deletions backend/was/src/was_django_project/qualys_admin/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Qualys administration application."""
Loading
Loading