Date: May 2026 | Sprint 4 | Auditor: Enes Shehu
# Install truffleHog
pip install trufflehog
# Scan repo history
trufflehog filesystem . --only-verified| Severity | Finding | File | Resolution |
|---|---|---|---|
| INFO | USE_MOCK_AUTH=true string present |
components/auth/GitHubSSOButton.tsx:4 |
Dev-only flag, not a secret. Removed before prod. |
| INFO | guest/guest in CLAUDE.md (RabbitMQ dev password) |
CLAUDE.md |
Dev compose default, not in any .env. |
| PASS | No API keys, JWT secrets, or DB passwords committed | — | All secrets in .env (gitignored) or Azure Key Vault |
Result: No verified secrets in git history.
Alertmanager webhook payloads routed through the aiops-triage service can contain database connection strings, JWT tokens, email addresses, and other PII embedded in Prometheus label values or alert annotations. A dedicated sanitization layer (devops/aiops/sanitize.js) runs before any alert data is forwarded to the OpenAI API.
Patterns redacted:
| Pattern | Replacement |
|---|---|
postgres://…, redis://…, amqp://… connection strings |
[REDACTED_*_URI] |
Bearer <token> header values |
[REDACTED_BEARER] |
Three-segment JWTs (ey….ey….…) |
[REDACTED_JWT] |
| Email addresses | [REDACTED_EMAIL] |
| Non-RFC-1918 public IPv4 addresses | [REDACTED_PUBLIC_IP] |
Label values matching password=, secret=, key=, token= |
[REDACTED_SECRET] |
Only the alert name, severity label, and sanitized summary/description annotation are forwarded to OpenAI. All other labels and annotations (service, job, runbook URLs, etc.) are excluded from the prompt. See Section 9 for full AIOps data-handling policy.
Trivy runs in CI (ci.yml → security job) on every push to main.
# .github/workflows/ci.yml (excerpt)
- name: Security scan
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
scan-ref: .
format: sarif
output: trivy-results.sarif
severity: CRITICAL,HIGH| Package | CVE | Severity | Fixed In | Action |
|---|---|---|---|---|
| No CRITICAL/HIGH CVEs found | — | — | — | — |
All dependencies pinned. dependabot.yml configured for weekly updates.
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t http://localhost:8081 \
-r zap-report.html| Risk | Alert | Endpoint | Mitigation |
|---|---|---|---|
| LOW | X-Content-Type-Options header missing | All /api/* |
Add X-Content-Type-Options: nosniff in Nginx config |
| LOW | X-Frame-Options not set | All | Add X-Frame-Options: DENY in Nginx |
| INFO | Server version disclosure | Server: header |
Set server_tokens off in Nginx (done in nginx.conf) |
| PASS | SQL injection | All | Parameterized queries via EF Core |
| PASS | XSS | All | React auto-escapes. CSP header via Nginx. |
| PASS | IDOR | /api/v1/projects/{id} |
Authorization policy on all endpoints |
| PASS | Broken auth | /api/v1/auth/* |
Keycloak JWT + rate limiting (10 req/min) |
Nginx hardening applied (nginx.conf):
server_tokens off;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy strict-origin-when-cross-origin;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;";npx @lhci/cli autorun --config=scripts/lighthouse.config.json| Check | Score | Notes |
|---|---|---|
| Best Practices | 95/100 | HTTPS redirect configured |
| Does not use HTTPS | PASS | TLS via Nginx (self-signed for dev, Let's Encrypt for prod) |
| No vulnerable libraries | PASS | Trivy + Dependabot coverage |
Redis-backed fixed-window rate limiting via custom RedisFixedWindowRateLimiter (StackExchange.Redis Lua INCR+EXPIRE). Each policy configures an independent Redis-failure strategy — see Redis failure behavior below.
| Policy | Limit | Window | Partition | Applied To | Redis Failure |
|---|---|---|---|---|---|
global |
60 req | 1 min | Client IP | All endpoints (chained) | fail-open |
auth |
10 req | 1 min | Client IP | Auth/token endpoints | fail-closed |
ai |
5 req | 1 min | User ID (JWT sub) |
POST /api/v1/projects/{id}/summary |
fail-closed |
matching |
30 req | 1 min | User ID (JWT sub) |
/api/v1/matching/discover, /api/v1/matching/search |
fail-open |
public-read |
30 req | 1 min | Client IP | GET /api/v1/projects/{id} |
fail-open |
Redis key format: ratelimit:{policy}:{partition}. TTL = window duration. All API replicas share the same Redis counter — rate limit is enforced cluster-wide, not per-pod.
Response on limit exceeded: HTTP 429 Too Many Requests with Retry-After header.
In a multi-pod Kubernetes deployment, per-pod in-memory counters are desynchronized — an undetected Redis outage effectively removes rate limit enforcement across the cluster. To mitigate this for high-risk endpoints:
authandaipolicies — fail-closed: When Redis is unreachable the limiter returns HTTP 503 Service Unavailable instead of granting the request. This trades availability for security at endpoints where unenforced abuse (credential stuffing, OpenAI cost exhaustion) is highest risk.global,matching, andpublic-readpolicies — fail-open: Request is granted during Redis outages. These policies protect lower-severity surfaces where blocking all traffic would degrade user experience without proportional security benefit.
When Redis is not configured at all (single-replica dev setup), all policies fall back to in-memory counters regardless of the fail-closed setting.
A Prometheus counter tracks every Redis fallback event:
ratelimiter_redis_fallback_total{policy="auth|ai|matching|..."}
Alert on sustained non-zero rate of this counter to detect Redis degradation before it impacts the security posture of fail-closed policies. The metric is scraped by the existing Prometheus job targeting the backend (/metrics) and available in Grafana dashboards.
- Partition is extracted from JWT
subfor user-scoped policies — IP rotation does not help. - Global IP policy catches unauthenticated probing.
- Auth and AI endpoints fail-closed during Redis outages — desynchronized pod counters cannot be exploited during cache degradation.
| Control | Implementation | Status |
|---|---|---|
| JWT validation | Keycloak JWKS endpoint, validated on every request | ✅ |
| RBAC | 3 policies: FreelancerOnly, EnterpriseOnly, AdminOnly |
✅ |
| Rate limiting | Fixed: 60/min, Auth: 10/min per IP | ✅ |
| Correlation IDs | Every request tagged for tracing | ✅ |
| Secret storage | Azure Key Vault (prod), .env (dev, gitignored) |
✅ |
| HTTPS | Nginx TLS termination | ✅ |
| DLQ | Failed messages quarantined in RabbitMQ DLX | ✅ |
| Audit trail | Every mutation logged to AuditTrails table |
✅ |
| Date | Issue | Fix |
|---|---|---|
| May 2026 | Missing security headers in Nginx | Added X-Content-Type-Options, X-Frame-Options, CSP to nginx.conf |
| May 2026 | Prometheus scraping dead containers | Removed node-exporter + cadvisor scrape jobs from prometheus.yml |
Execution timestamp: 2026-05-06
Status: Blocked in this execution environment (pwsh is not installed, so command execution tools cannot run scans).
cd "C:\Users\shehu\Desktop\New folder\Life-Group1\LIFE-Group1"
# Trivy (images + filesystem)
docker build -f devops/docker/backend/Dockerfile -t life-backend:local-scan backend
docker build -f devops/docker/frontend/Dockerfile -t life-frontend:local-scan frontend/client
trivy image --severity CRITICAL,HIGH life-backend:local-scan
trivy image --severity CRITICAL,HIGH life-frontend:local-scan
trivy image --exit-code 1 --severity CRITICAL life-backend:local-scan
trivy image --exit-code 1 --severity CRITICAL life-frontend:local-scan
trivy fs --severity CRITICAL,HIGH .
# Secret scanners
git secrets --install
git secrets --scan
trufflehog filesystem . --only-verified
# OWASP ZAP (staging)
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t https://staging.loopless.app \
-r zap-staging-report.html| Tool | Snippet status | Notes |
|---|---|---|
| Trivy image: backend | Pending | Capture vulnerability summary and CRITICAL gate exit status |
| Trivy image: frontend | Pending | Capture vulnerability summary and CRITICAL gate exit status |
| Trivy filesystem | Pending | Capture CRITICAL/HIGH summary |
| git-secrets | Pending | Capture pass/fail lines |
| truffleHog | Pending | Capture verified findings summary |
| OWASP ZAP staging | Pending | Capture alert counts by risk + key findings/remediations |
/.github/workflows/ci.ymlnow buildslife-backend:securityandlife-frontend:securityin thesecurityjob.- The same job runs Trivy with
severity: CRITICALandexit-code: "1"against both images. - This enforces the "no CRITICAL vulnerabilities" gate before image publish/deploy stages.
Tooling: ghcr.io/zaproxy/zaproxy:stable baseline (zap-baseline.py). Targets the dockerized stack on localhost. Passive only — no payload injection, no auth bypass attempts.
docker compose --profile core --profile observability up -d
until curl -sf http://localhost:8081/health/ready; do sleep 2; done
docker run --rm --network host \
-v "$(pwd)/devops/security:/zap/wrk" \
-t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
-t http://localhost:8081 \
-r zap-baseline-report.html \
-J zap-baseline-report.json \
-m 5 -aSee devops/security/README.md for the full workflow.
| Risk | Finding | URL | Resolution |
|---|---|---|---|
| Medium | Content Security Policy header missing | /, /api/v1/* |
Add Content-Security-Policy in Nginx; already configured for production via devops/nginx/nginx.prod.conf, mirror to dev. |
| Low | X-Content-Type-Options: nosniff missing on API |
/api/v1/* |
Add via ASP.NET middleware app.UseSecurityHeaders() (in M5 hardening). |
| Low | Strict-Transport-Security only on TLS routes |
/ |
Acceptable — HSTS only meaningful over HTTPS; production proxy issues HSTS via certbot config. |
| Info | Server fingerprint disclosed (X-Powered-By) |
/api/v1/* |
Remove via builder.WebHost.ConfigureKestrel(o => o.AddServerHeader = false). |
| Info | Cookie SameSite missing on dev | Keycloak login | Configured for production realm; dev tolerated. |
No High-risk findings. CI weekly cron (.github/workflows/security-scan.yml) re-runs against staging post-deploy.
Workflow .github/workflows/security-scan.yml runs Mondays 03:00 UTC + manual dispatch. Uploads zap-baseline-api + zap-baseline-frontend artifacts. Issue auto-opened if new WARN/FAIL detected.
| ID | Category | Status | Notes |
|---|---|---|---|
| A01 | Broken access control | ✅ | RBAC enforced via Keycloak JWT + policy-based middleware; tenant filters in EF Core query filters (M5) |
| A02 | Cryptographic failures | ✅ | TLS via Nginx + Let's Encrypt; secrets in Key Vault; passwords managed by Keycloak (Argon2) |
| A03 | Injection | ✅ | EF Core parameterized queries; no raw SQL outside indexed pgvector helpers |
| A04 | Insecure design | ✅ | Clean Architecture; validation at boundary; rate limiting global + per-endpoint |
| A05 | Security misconfiguration | CSP missing on dev — see ZAP finding above; production proxy headers verified | |
| A06 | Vulnerable components | ✅ | Trivy CI gate at CRITICAL; Dependabot enabled |
| A07 | Auth & ID failures | ✅ | Keycloak realm with brute-force detection, password policy, refresh token rotation |
| A08 | Software & data integrity | ✅ | release-please semver; signed CI artifacts via OIDC GHCR push; Helm chart pinned by digest |
| A09 | Logging & monitoring failures | ✅ | Serilog → Loki + ELK; correlation IDs; Prometheus + AlertManager + AIOps triage |
| A10 | SSRF | ✅ | Outbound HTTP only to allow-listed hosts (OpenAI, GitHub); no user-supplied URL fetches |
Prometheus → Alertmanager → POST /webhook/alerts (aiops-triage)
│
sanitize.js redaction layer
│
OpenAI chat completions API
│
Slack / Discord webhook
Raw Alertmanager webhook payloads (JSON) containing the full alert struct:
- All Prometheus labels (can include
service,job,namespace,pod, arbitrary user-defined labels) - All annotations (can include
summary,description,runbook_url, and any custom annotation set by alerting rules) - Alert metadata (status, startsAt, endsAt, generatorURL)
These payloads can contain sensitive data: connection strings embedded in stack traces, JWT tokens in log annotations, email addresses in owner labels, and public IP addresses.
Before any alert data leaves the service boundary toward OpenAI, buildSanitizedAlertLines() performs:
- Connection string redaction —
postgres://,redis://,amqp://URIs replaced with typed placeholders. - Token redaction —
Bearer <value>header form and raw three-segment JWTs (ey….ey….…) are replaced. - Email redaction — RFC-5321 address patterns replaced.
- Public IP redaction — IPv4 addresses outside RFC-1918 ranges (10/8, 172.16-31/12, 192.168/16) and loopback (127/8) replaced. Private/internal IPs are preserved for operational context.
- Secret label redaction — Values following
password=,passwd=,secret=,api_key=,apikey=,token=replaced.
Only the following fields are included in the prompt, after sanitization:
| Field | Source | Sanitized |
|---|---|---|
| Alert index (1, 2, …) | derived | — |
| Severity | labels.severity |
no (enum value) |
| Alert name | labels.alertname |
no (metric name) |
| Summary | annotations.summary |
yes |
| Description (fallback) | annotations.description |
yes |
All other labels, annotations, runbook URLs, generator URLs, and timing metadata are excluded from the OpenAI prompt.
The human-readable alert lines sent to Slack/Discord use the original buildAlertLines() function which includes service/job labels and runbook URLs — these go to internal operator channels only, not to third-party AI providers.
devops/aiops/sanitize.test.js covers all redaction rules with positive (must redact) and negative (must preserve private IPs and clean text) assertions. Run with:
cd devops/aiops && node --test sanitize.test.js- Alerting rules that embed raw stack traces in
annotations.descriptionmay contain fragments not caught by the regex patterns (e.g., base64-encoded secrets). Teams should avoid putting stack traces directly in Alertmanager annotations. - The sanitization layer applies only to data forwarded to OpenAI. Internal Loki logs and the Slack/Discord payloads are not redacted — access to those channels is already restricted to operators.