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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions .github/workflows/terraform-translator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: terraform-translator

on:
push:
paths:
- "terraform-translator/**"
- ".github/workflows/terraform-translator.yml"
pull_request:
paths:
- "terraform-translator/**"
- ".github/workflows/terraform-translator.yml"

defaults:
run:
working-directory: terraform-translator

jobs:
# Always runs — needs no secrets. The translator lazily imports the Anthropic
# SDK, so the offline suite runs without dependencies or an API key.
offline-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Run offline unit tests
run: python -m unittest discover -s tests -v

# Runs only when an ANTHROPIC_API_KEY repository secret is configured.
# Generates real Terraform via the CLI and validates it with the Terraform CLI.
live-check:
runs-on: ubuntu-latest
needs: offline-tests
steps:
- uses: actions/checkout@v4

- name: Detect API key
id: detect
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
if [ -n "$ANTHROPIC_API_KEY" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::notice::No ANTHROPIC_API_KEY secret set — skipping live check."
fi

- uses: actions/setup-python@v5
if: steps.detect.outputs.present == 'true'
with:
python-version: "3.11"

- name: Install dependencies
if: steps.detect.outputs.present == 'true'
run: pip install -r requirements.txt

- name: Generate Terraform from a prompt
if: steps.detect.outputs.present == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
mkdir -p tf-check
python cli.py "a resource group named demo-rg in West Europe with a storage account" > tf-check/main.tf
echo "----- generated main.tf -----"
cat tf-check/main.tf

- uses: hashicorp/setup-terraform@v3
if: steps.detect.outputs.present == 'true'

- name: Validate generated HCL
if: steps.detect.outputs.present == 'true'
working-directory: terraform-translator/tf-check
run: |
terraform fmt -check
terraform init -backend=false
terraform validate
19 changes: 19 additions & 0 deletions iac-aistudio-update/.gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# GitLab CI — runs the security regression tests on every push/MR.
# No secrets required: the suite runs in LOCAL_DEV mode (SQLite, dummy secret).
stages:
- test

security-tests:
stage: test
image: python:3.12-slim
variables:
LOCAL_DEV: "1"
FLASK_SECRET_KEY: "ci-test-secret"
before_script:
- pip install --no-cache-dir -r requirements-dev.txt
script:
# Run from the bundle root so `import app` and templates/ resolve.
- pytest -v
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
133 changes: 133 additions & 0 deletions iac-aistudio-update/IMPLEMENTATION_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# IaC Build — security hardening update (deploy guide)

This bundle contains a hardened `app.py` plus tests. It is a **drop-in replacement
for `app.py`** in your existing deploy bundle — nothing else in your bundle
(Dockerfile, templates, `landing.html`, `app.html`, `requirements.txt`) needs to
change, and **no new environment variables or Azure resources are required** (all
new settings have safe built-in defaults).

> Scope note: this pass covers the changes that are pure code and low-risk to a
> live deploy. It deliberately does **not** change the model/cost profile, touch
> your SSE streaming, or add infrastructure. See "Still open" at the bottom.

---

## 1. What changed (and why)

All changes are in `app.py`. Diff it against your current file before deploying
(`diff -u old/app.py app.py`).

| Area | Change | Security-plan item |
|------|--------|--------------------|
| **LLM model pin** | `/api/generate` now uses a **server-pinned** model (`LLM_MODEL`, default `claude-sonnet-4-6`). The client-supplied `model` field is ignored. | P1-5 |
| **OTP strength** | Codes are now **6-digit, cryptographically random** (`secrets`), stored as an **HMAC** (never plaintext), with **server-side expiry** (10 min) and **lockout** after 5 wrong attempts. Single-use. | P0-1 |
| **Password-reset authorization** | `/recover_reset` now **requires a verified OTP in the session**. Previously the reset step did not check that the code had been entered, so anyone who set `recover_email` (a plain POST to `/recover_lookup`) could POST `/recover_reset` and change another account's password. This is the most important fix here. | (gap found during review) |
| **Registration authorization** | `/profile` (account creation) likewise requires a verified OTP, so accounts can't be created for an unverified email by skipping the code step. | P0-1 related |
| **Session fixation** | `login()` and the OAuth callback now rotate the session (`session.clear()` then re-issue) via a shared `_login_user()` helper. | P1-3 |
| **Email enumeration** | `/recover_lookup` returns a **uniform** "if that account exists, a code has been sent" response whether or not the email exists. | P2 (upgrades) |
| **Session version** | `SESSION_VERSION` bumped `3 → 4`, so all pre-deploy sessions are invalidated once (everyone is logged out a single time after deploy). | — |

New env vars (all **optional**, defaults shown):

| Env var | Default | Purpose |
|---------|---------|---------|
| `LLM_MODEL` | `claude-sonnet-4-6` | The pinned generation model. |
| `LLM_MAX_TOKENS` | `8000` | Hard clamp on output tokens (unchanged value). |
| `LLM_DEFAULT_TOKENS` | `4000` | Default when the client omits `max_tokens` (unchanged value). |
| `OTP_TTL_SECONDS` | `600` | OTP validity window. |
| `OTP_MAX_ATTEMPTS` | `5` | Wrong attempts before lockout. |

---

## 2. Test before you ship (optional but recommended)

From the bundle root (where `app.py` and `templates/` are):

```bash
pip install -r requirements-dev.txt
LOCAL_DEV=1 FLASK_SECRET_KEY=test-secret pytest -v
```

This exercises the OTP logic, the reset-authorization fix, and the model pin
with no Azure dependencies and no network calls.

---

## 3. Deploy (Azure Cloud Shell)

This follows your existing build/deploy pattern. Replace only `app.py` in the
bundle, then rebuild and roll the revision.

```bash
# 0) Note the current image so you can roll back instantly if needed.
PREV=$(az containerapp show -n ca-iacb-web -g rg-iacb-lean \
--query "properties.template.containers[0].image" -o tsv)
echo "rollback image: $PREV"

# 1) Unzip your current deploy bundle (adjust the zip name/path to yours).
rm -rf ~/iacb && mkdir -p ~/iacb && unzip -o ~/your-deploy-bundle.zip -d ~/iacb

# 2) Drop in the updated app.py (from this update bundle).
cp /path/to/iac-aistudio-update/app.py ~/iacb/app.py
cd ~/iacb

# 3) Build the image in ACR (Dockerfile base must already exist in ACR:
# FROM acriacblean.azurecr.io/python:3.12-slim-bookworm).
TAG=$(date +%Y%m%d-%H%M)
az acr build --registry acriacblean --image iacb-web:$TAG .

# 4) Roll the Container App to the new image.
az containerapp update -n ca-iacb-web -g rg-iacb-lean \
--image "acriacblean.azurecr.io/iacb-web:$TAG"
```

Optional — override any of the new settings (not required; defaults are baked in):

```bash
az containerapp update -n ca-iacb-web -g rg-iacb-lean \
--set-env-vars LLM_MODEL=claude-sonnet-4-6 OTP_TTL_SECONDS=600 OTP_MAX_ATTEMPTS=5
```

---

## 4. Post-deploy verification

```bash
# Health (also proves DB connectivity)
curl -fsS https://iac-aistudio.com/healthz && echo
```

Then click through once:

1. **Register** a throwaway account → confirm the email code is now **6 digits**,
that a **wrong** code is rejected, and that completing the flow logs you in.
2. **Forgot password** for that account → confirm reset works **only after**
entering the code, and that entering an **unknown** email gives the same
"if that account exists…" message (no "no account found").
3. **Reset bypass is closed**: a direct POST to `/recover_reset` without first
verifying a code must NOT change any password (the test asserts this too).
4. **Model pin**: in App Insights / container logs, confirm generations run on
`LLM_MODEL` regardless of what the client sends.
5. Everyone is logged out once (expected — `SESSION_VERSION` bump).

---

## 5. Rollback (instant)

```bash
az containerapp update -n ca-iacb-web -g rg-iacb-lean --image "$PREV"
```

---

## 6. Still open (not in this code drop)

- **P0-2 — shared rate-limit store.** The limiter is still in-memory (per
replica). This needs an **Azure Cache for Redis** resource provisioned first,
then `storage_uri="redis://…"` on the `Limiter(...)`. Infrastructure, not a
pure code change — do it as a follow-up.
- **CSP `'unsafe-inline'`** on `script-src` (P1-4) — requires externalizing
inline JS to `/static/js/*.js` and moving to nonces. Large, deferred.
- **Central input-validation layer** (P1-5 cont.) — Pydantic/marshmallow on POST
bodies. Deferred.
- **Monthly LLM spend cap / WAF / image scanning** — roadmap items.
37 changes: 37 additions & 0 deletions iac-aistudio-update/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# IaC Build — security hardening update

Drop-in replacement for `app.py` in the IaC-AIStudio deploy bundle, plus tests
and CI. Hardens authentication and pins the LLM model server-side, with no new
Azure resources and no required config changes.

**Start here:** [`IMPLEMENTATION_GUIDE.md`](./IMPLEMENTATION_GUIDE.md) — what
changed, how to test, and the exact Cloud Shell deploy + rollback steps.

## Contents

| File | What it is |
|------|------------|
| `app.py` | Hardened application (replaces your current `app.py`) |
| `IMPLEMENTATION_GUIDE.md` | Deploy guide + change log + verification checklist |
| `tests/test_security.py` | Security regression tests (OTP, reset auth, model pin) |
| `requirements-dev.txt` | Minimal deps to run the tests in `LOCAL_DEV` mode |
| `.gitlab-ci.yml` | Runs the tests on every push/MR (no secrets) |
| `requirements.txt` | Your runtime deps (unchanged copy, for reference) |

## Quick test

```bash
pip install -r requirements-dev.txt
LOCAL_DEV=1 FLASK_SECRET_KEY=test-secret pytest -v
```

## Summary of changes

- **Model pin (P1-5):** `/api/generate` ignores the client `model`; pinned via `LLM_MODEL`.
- **OTP (P0-1):** 6-digit crypto codes, HMAC-stored, 10-min expiry, 5-attempt lockout, single-use.
- **Reset/registration authorization:** both now require a verified OTP (closes a password-reset bypass).
- **Session fixation (P1-3):** session rotates on login and OAuth callback.
- **Email enumeration:** uniform recovery response.

See the guide for the full table and the items still left open (Redis limiter,
CSP nonces, validation layer).
84 changes: 84 additions & 0 deletions iac-aistudio-update/SETUP-VSCODE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Move the IaC Build app to a git repo + VS Code (deploy without Cloud Shell)

Today the app is deployed from a **zip bundle**. This sets it up as a proper git
repo you edit in VS Code and deploy from the VS Code integrated terminal.

## Prerequisites (install once on your workstation)

- **Azure CLI** (`az`) — https://learn.microsoft.com/cli/azure/install-azure-cli
- **Git**
- **VS Code** + extensions: Claude Code (you have it), and optionally
*Azure Container Apps*, *Docker*, and *Python*.
- You do **not** need Docker Desktop — `az acr build` builds in Azure.

## 1. Make the app a git repo

Use the bundle you already unzipped (it has the hardened `app.py`):

```bash
cd ~/iacb/code # the folder with Dockerfile, app.py, app.html, templates/

cp /path/to/iac-aistudio-update/deploy.sh ./deploy.sh && chmod +x deploy.sh
cp /path/to/iac-aistudio-update/app.gitignore ./.gitignore

git init -b main
git add .
git commit -m "IaC Build app (hardened) — initial repo"
```

> Keep the repo **private** — it contains your application logic (no secrets:
> those stay in Key Vault). Do not commit `.env` files or local databases
> (the `.gitignore` handles this).

## 2. Push to your git host (pick one)

**GitLab** (you mentioned this):
```bash
git remote add origin https://gitlab.com/<you>/iac-build.git
git push -u origin main
```

**GitHub:**
```bash
git remote add origin https://github.com/<you>/iac-build.git
git push -u origin main
```

## 3. Open in VS Code

```bash
code ~/iacb/code
```
Or in VS Code: **File ▸ Open Folder…** → select the repo. Use the Claude Code
extension in that window to make changes.

## 4. Deploy from VS Code (replaces Cloud Shell)

In the VS Code integrated terminal (**Ctrl+`**):

```bash
az login # once per session/workstation
az account set --subscription 35b00a54-aaaf-46cb-9ec3-783ab739b084
./deploy.sh
```

`deploy.sh` builds in ACR, rolls the Container App, prints revision health, and
shows the exact rollback command. That's your whole deploy now — edit in VS
Code, commit, run `./deploy.sh`.

## 5. Day-to-day loop

1. Edit code in VS Code (Claude Code can make the changes).
2. `git commit` (and `git push` to back it up).
3. `./deploy.sh` in the terminal.
4. Verify: the script's health line, plus a click-through on https://iac-aistudio.com.

Rollback any time: `az containerapp update -n ca-iacb-web -g rg-iacb-lean --image "<previous-tag>"`.

## Notes

- The base image `python:3.12-slim-bookworm` now lives in your ACR. If a build
ever fails with `manifest unknown`, uncomment the `az acr import` line in
`deploy.sh` and run it once.
- For automated checks on every push, wire up the security tests
(`tests/test_security.py` + `.gitlab-ci.yml`) from this update bundle.
8 changes: 8 additions & 0 deletions iac-aistudio-update/app.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Rename to ".gitattributes" in your app repo root.
# Forces LF line endings so files built into the Linux container image stay
# correct even when edited on Windows.
* text=auto
*.py text eol=lf
*.sh text eol=lf
*.html text eol=lf
Dockerfile text eol=lf
15 changes: 15 additions & 0 deletions iac-aistudio-update/app.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Rename this to ".gitignore" in your app repo root.
# Keeps secrets, local venvs, and build cruft out of git.
__pycache__/
*.pyc
.venv/
venv/
env/
.env
.env.*
*.local
.DS_Store
.pytest_cache/
# Don't commit local SQLite dev databases
users.db
instance/
Loading
Loading