Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .github/workflows/docker-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ jobs:
target: web
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Attach SLSA build provenance + SBOM so a swapped/backdoored image can be
# detected against the attestation at deploy time (chain C-005).
provenance: true
sbom: true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
config.json

# Real secrets must never be committed (chain C-004) — only *.sample templates.
secrets/*.env

__pycache__/
*.py[cod]
db.sqlite3
Expand Down
9 changes: 9 additions & 0 deletions Access/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,18 @@ def pending_requests(request):


@login_required
@user_any_approver
def accept_bulk(request, selector):
"""approve one or more access request.

F-022: gate the bulk-approval endpoint with @user_any_approver (matching the
sibling views pending_requests / decline_access) so a logged-in NON-approver
cannot reach the approval paths at all. This checks "holds ANY approver
permission" via getPossibleApproverPermissions(), so module-specific approvers
are unaffected. Per-request authorization (module/group approver perms, primary/
secondary checks, self-approval block) is still enforced in the helpers below —
this is a defense-in-depth view gate, not a replacement for those checks.

Args:
request (HTTPRequest): Details of access and access approver.
selector (str): access type being approved.
Expand Down
11 changes: 8 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ RUN DEBIAN_FRONTEND=noninteractive \
# Set env variables used in this Dockerfile (add a unique prefix, such as DEV)
RUN apt update && apt install -y netcat dnsutils libmariadb-dev

RUN mkdir -p /ebs/logs && touch /ebs/logs/engima.log && chmod 777 /ebs/logs/engima.log

ARG APPUID=1001
RUN useradd -rm -d /home/app -s /bin/bash -g root -u "$APPUID" app
# Own group for the app user (not gid 0 / root group) so a container escape
# does not inherit root-group file access (chains C-001 / C-003).
RUN groupadd -g "$APPUID" app \
&& useradd -rm -d /home/app -s /bin/bash -g app -u "$APPUID" app

# Log file owned by the app user, not world-writable (was chmod 777).
RUN mkdir -p /ebs/logs && touch /ebs/logs/engima.log \
&& chown -R app:app /ebs/logs && chmod 640 /ebs/logs/engima.log
WORKDIR /srv/code/dev
RUN mkdir -p logs
RUN mkdir -p db
Expand Down
37 changes: 34 additions & 3 deletions EnigmaAutomation/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,30 @@
CSRF_COOKIE_HTTPONLY = True
SESSION_COOKIE_HTTPONLY = True

# --- Response security headers (chain C-002 / F-035) ---
# Sent by django.middleware.security.SecurityMiddleware + clickjacking middleware.
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_REFERRER_POLICY = "same-origin"
X_FRAME_OPTIONS = "DENY"

# --- Content-Security-Policy (django-csp / csp.middleware.CSPMiddleware) ---
# NOTE: 'unsafe-inline' is retained for script/style because the ops UI still
# ships inline <script> blocks and inline styles (16 templates). Even so this
# policy blocks injected EXTERNAL scripts, object/embed, <base> hijacking and
# clickjacking. Follow-up: migrate templates to nonces and drop 'unsafe-inline'.
# The allowed CDN hosts below reflect the jQuery/FontAwesome assets the UI loads
# — smoke-test the pages render before flipping CSP_REPORT_ONLY off.
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'", "'unsafe-inline'", "code.jquery.com", "cdn.jsdelivr.net")
CSP_STYLE_SRC = ("'self'", "'unsafe-inline'", "fonts.googleapis.com", "use.fontawesome.com", "cdn.jsdelivr.net")
CSP_FONT_SRC = ("'self'", "data:", "fonts.gstatic.com", "use.fontawesome.com")
CSP_IMG_SRC = ("'self'", "data:")
CSP_CONNECT_SRC = ("'self'",)
CSP_OBJECT_SRC = ("'none'",)
CSP_BASE_URI = ("'self'",)
CSP_FRAME_ANCESTORS = ("'none'",)
CSP_FORM_ACTION = ("'self'",)

# Application definition

INSTALLED_APPS = [
Expand All @@ -72,12 +96,16 @@
CID_CONCATENATE_IDS = True
SESSION_EXPIRE_SECONDS = 7*24*60*60
SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True
AXES_ONLY_USER_FAILURES=True
# False so lockout also factors in source IP (username+IP), not username-only —
# username-only tracking lets credential-stuffing spread one attempt across many
# usernames from a single IP without ever tripping the limit (chain C-008 / F-032).
AXES_ONLY_USER_FAILURES=False
AXES_FAILURE_LIMIT=5
AXES_COOLOFF_TIME=2.0

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"csp.middleware.CSPMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware',
Expand Down Expand Up @@ -107,7 +135,8 @@
"social_core.pipeline.social_auth.associate_user",
"social_core.pipeline.social_auth.load_extra_data",
"social_core.pipeline.user.user_details",
"social_core.pipeline.debug.debug",
# NOTE: social_core.pipeline.debug.debug removed (chain C-008) — it logs the
# full OAuth response, including access tokens and user PII, at DEBUG level.
)

SOCIAL_AUTH_DISCONNECT_PIPELINE = (
Expand Down Expand Up @@ -268,7 +297,9 @@

AUTOMATED_EXEC_IDENTIFIER = "automated-grant"

current_log_level = 'DEBUG'
# INFO (not DEBUG) in the app loggers — DEBUG emits request/response bodies and
# third-party token traffic that can leak secrets to the log sink (chain C-008).
current_log_level = 'INFO'
logging_apps = ["django.request", "inventory", "Access", "bootprocess"]
LOGGING = {
'version': 1,
Expand Down
7 changes: 5 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ services:
db:
container_name: db
image: mysql/mysql-server:8.0.31
# Bind to loopback only — the DB must not be reachable from the network
# (chain C-004). Containers reach it via the compose network as `db:3306`.
ports:
- 3306:3306
- 127.0.0.1:3306:3306
env_file:
- ./secrets/ops_mysql_dev.env
volumes:
Expand All @@ -37,8 +39,9 @@ services:
container_name: redis
image: redis:alpine
command: --port 6379
# Loopback only (chain C-004); the celery worker reaches it as `redis:6379`.
ports:
- 6379:6379
- 127.0.0.1:6379:6379
read_only: true
security_opt:
- no-new-privileges:true
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ mypy==2.3.0
mypy-extensions==1.1.0
vulture==2.16
django-cid==2.3
django-csp==3.8
flake8==7.3.0
django-session-timeout==0.1.0
django-axes==5.41.1
Expand Down
5 changes: 0 additions & 5 deletions secrets/ops_app_celery.env

This file was deleted.

5 changes: 5 additions & 0 deletions secrets/ops_app_celery.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/1
MYSQL_ROOT_PASSWORD=__CHANGE_ME__
MYSQL_DATABASE=enigma
MYSQL_ROOT_HOST=%
2 changes: 0 additions & 2 deletions secrets/ops_app_dev.env

This file was deleted.

2 changes: 2 additions & 0 deletions secrets/ops_app_dev.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TOKEN=__CHANGE_ME__
DEBUG=True
2 changes: 0 additions & 2 deletions secrets/ops_app_test.env

This file was deleted.

2 changes: 2 additions & 0 deletions secrets/ops_app_test.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TOKEN=__CHANGE_ME__
DEBUG=True
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
MYSQL_ROOT_PASSWORD=testtest
MYSQL_ROOT_PASSWORD=__CHANGE_ME__
MYSQL_DATABASE=enigma
MYSQL_ROOT_HOST=%
Loading