From f60ba704e976e3f253e0e78c443ead277c4acfc0 Mon Sep 17 00:00:00 2001 From: nogueiraguh Date: Mon, 6 Apr 2026 15:26:55 -0300 Subject: [PATCH] =?UTF-8?q?A-Team=20Security=20=E2=80=94=20Infrastructure?= =?UTF-8?q?=20&=20Security=20Agent=20Engineer=20v1.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 574 ++++++++++++++++++++++++++ logo.svg | 21 + ocir-login.py | 131 ++++++ setup.sh | 97 +++++ terraform/compute.tf | 78 ++++ terraform/dns.tf | 36 ++ terraform/lb.tf | 143 +++++++ terraform/network.tf | 189 +++++++++ terraform/outputs.tf | 56 +++ terraform/provider.tf | 22 + terraform/storage.tf | 22 + terraform/templates/cloud-init.sh.tpl | 141 +++++++ terraform/terraform.tfvars.example | 55 +++ terraform/variables.tf | 185 +++++++++ terraform/waf.tf | 120 ++++++ 15 files changed, 1870 insertions(+) create mode 100644 README.md create mode 100644 logo.svg create mode 100755 ocir-login.py create mode 100755 setup.sh create mode 100644 terraform/compute.tf create mode 100644 terraform/dns.tf create mode 100644 terraform/lb.tf create mode 100644 terraform/network.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/provider.tf create mode 100644 terraform/storage.tf create mode 100644 terraform/templates/cloud-init.sh.tpl create mode 100644 terraform/terraform.tfvars.example create mode 100644 terraform/variables.tf create mode 100644 terraform/waf.tf diff --git a/README.md b/README.md new file mode 100644 index 0000000..420ffe8 --- /dev/null +++ b/README.md @@ -0,0 +1,574 @@ +

+ A-Team Security — Infrastructure & Security Agent Engineer +

+ +

A-Team Security — Infrastructure & Security Agent Engineer

+ +

+ Oracle Cloud Infrastructure — CIS Foundations Benchmark 3.0 — AI-Powered Compliance Platform +

+ +

+ Version + OCI + Docker + Terraform + License +

+ +--- + +## Overview + +A-Team Security Agent is a self-hosted web application that automates **CIS Oracle Cloud Infrastructure Foundations Benchmark 3.0** compliance checks, powered by **OCI Generative AI** for intelligent analysis and an **MCP (Model Context Protocol)** server architecture for extensible task execution. + +The platform combines security compliance scanning, AI-powered chat with **RAG (Retrieval-Augmented Generation)**, infrastructure exploration, and vector-based knowledge storage into a single Docker image with a **React 19 SPA**, **Oracle Dark Premium** theme (light/dark modes), **KPI dashboard** with compliance gauge, **i18n** (pt/en/es), and **Recharts** visualizations. + +--- + +## Container Image + +The platform is distributed as a single Docker image on **Oracle Container Registry (OCIR)**: + +``` +us-ashburn-1.ocir.io/idi1o0a010nx/oci-cis-agent:latest +``` + +- **Multi-architecture**: `linux/amd64` + `linux/arm64` +- **Single container**: nginx (frontend) + FastAPI (backend) + supervisord +- **Port**: `8080` +- **Volume**: `/data` (persistent storage — database, configs, reports, wallets) + +--- + +## Features + +### AI Chat Agent with RAG + MCP Tool Use +- **OCI Generative AI** integration via official SDK (`oci.generative_ai_inference`) +- **RAG (Retrieval-Augmented Generation)**: queries ADB vector store for relevant context before generating responses — single ADB connection per search, smart table skip, CIS recommendation text filter for exact matching, follow-up context enrichment +- **Source hierarchy**: findings tables > cisrecom (official remediation) > engineerknowledgebase (complementary) — temporal awareness with extract dates, tenancy isolation via JSON_VALUE filter +- **MCP Tool Use (Function Calling)**: GenAI models call tools from registered MCP servers during chat — supports Cohere and Generic (OpenAI-style) function calling with automatic tool execution loop (max 5 iterations) +- **Chat Memory Compaction**: automatic summarization of older messages when conversation exceeds ~6000 tokens — keeps 20 recent messages intact +- **Chat Sidebar**: collapsible panel with model parameters (temperature, max_tokens, top_p, top_k, penalties) +- **Multimodal Chat**: upload images (PNG/JPG/GIF/WebP), PDFs, and text files for AI analysis — up to 5 files per message via OCI GenAI `ImageContent` and `DocumentContent` +- **Async Background Processing**: chat requests return immediately, GenAI + MCP tools process in background via dedicated thread pool (10 threads), frontend polls for results +- 16 chat models + 3 embedding models across 5 providers: **Meta** (Llama 4), **Google** (Gemini 2.5), **OpenAI** (GPT-5.2/5.1/5 Mini/4.1/4o, o3/o4-mini), **xAI** (Grok 4/3) +- Full parameter control: temperature, max_tokens, top_p, top_k, frequency/presence penalty (when supported) +- Toggle MCP tools on/off per chat session +- Conversation history with session management +- On-Demand and Dedicated serving modes + +### Terraform Agent (IaC) +- **AI-powered Terraform code generation** for OCI infrastructure provisioning +- Dedicated chat interface with OCI Terraform provider context +- **Workspace management**: create, plan, apply, destroy Terraform workspaces +- **Compartment-aware**: browse and select target compartment, with existing OCI resources injected as context +- **Plan/Apply/Destroy lifecycle**: full Terraform workflow with real-time output tracking +- **File management**: view generated `.tf` files, copy, download individually or as bundle +- **Inline file editor**: click any `.tf` file to edit in-place with monospace editor, Tab support, and save +- **Split-panel terminal**: real-time Plan/Apply/Destroy output in a dark terminal panel alongside files/plan/resources +- **Multi-file generation**: AI generates separate `.tf` files via `// filename:` markers (main.tf, variables.tf, outputs.tf, etc.) +- **Smart file correction**: when fixing errors, model generates only changed files — frontend merges with existing workspace, preserving untouched files +- **14-point validation checklist**: system prompt enforces cross-references, CIDRs, security lists, route tables, DRG attachments, RPC peering, HCL syntax, resource type validation, and variable declarations +- **Resource type validation**: SQLite-based validation of ~937 OCI resource types with `difflib.get_close_matches` suggestions +- **Terraform Resource Reference**: auto-generated OCI provider resource catalog (~937 resources) injected into system prompt +- **Official Resource Docs Injection**: on-demand fetch of Example Usage + Argument Reference from the official OCI Terraform provider docs — cached in SQLite +- **Prompt Generator**: dedicated sub-menu for AI-powered prompt generation +- Terraform CLI installed in container (v1.14.7) + +### OCI Account Explorer +- **KPI stats bar**: real-time resource counts per category with tooltip breakdown +- **36 resource types** across 9 categories: + - **Compute**: Instances, Boot Volumes, Instance Pools + - **Networking**: VCNs, Subnets, Security Lists, NSGs, Route Tables, NAT/Internet/Service Gateways, Public IPs, Load Balancers + - **Storage**: Buckets, Block Volumes, File Systems, Mount Targets + - **Database**: Autonomous Databases, DB Systems, MySQL DB Systems + - **Containers**: Container Instances, OKE Clusters + - **Serverless**: Functions, API Gateways + - **Observability**: Alarms, Log Groups, Notification Topics, Events Rules + - **Security**: Vaults, DNS Zones, Network Firewalls, Firewall Policies + - **IAM**: Users, Groups, Policies, Dynamic Groups +- **Start/Stop** Compute Instances, Autonomous Databases, DB Systems, MySQL, Container Instances +- **Tree-view navigation** with resizable compartment panel +- **Multi-region support** with checkbox selection + +### OCI CLI Terminal +- **Linux-style web terminal** for OCI CLI interaction directly from the browser +- **Tab autocomplete**: auto-completes OCI CLI commands and subcommands +- **OCID auto-lookup**: paste any OCID (60+ resource types) — auto-detects type and runs the appropriate `get` command +- **`find` by name/IP**: search OCI resources by display name or IP address via OCI Search API +- Per-user command history, state persists across navigation + +### OCI Services +- **Service Status**: auto-detect 6 security services (VSS, Data Safe, Cloud Guard, Bastion, Security Zones, Vault) per tenancy via OCI API with user overrides +- **OCI Health**: real-time Oracle service health from ocistatus.oraclecloud.com — 49 regions, search, geographic filters + +### CIS Compliance Reports (Oracle Official Engine) +- Powered by Oracle's official `cis_reports.py` (48 CIS + 11 OBP checks) +- **Granular execution parameters**: CIS Level (1/2), OCI Best Practices, Raw Data, OCID Redaction +- **Multiple output formats**: HTML summary, CSV per section/finding, JSON summary, optional XLSX +- **Report history**: full execution history with status, tenancy filter, and download actions +- Region filtering with multi-select +- Real-time progress tracking with phase-based progress bar +- **CIS Engine auto-update**: check for new versions from Oracle's GitHub and update with one click + +### LAD A-Team CIS Compliance Report +- **Professional compliance report** following Oracle Cloud Security Assessment format (cover page, purpose statement, disclaimer, copyright) +- **Table of Contents** with links to each section and recommendation +- **Security Overview**: 7 Oracle security pillars +- **OCI Services section**: summary table of 6 security services per tenancy + detailed descriptions + Cloud Guard recommendations table +- **CIS Assessment Summary**: Domains / Total Controls / Failed / Passed per section +- **Detailed findings**: per-recommendation cards with compliance percentage bar, result description, and remediation steps +- **RAG-powered remediation**: enriched remediation steps from ADB vector store +- **Download PDF** and **Download DOCX** with professional formatting +- **Progress bar during generation**: shows RAG progress, percentage, and current recommendation + +### Built-in CIS MCP Server (Granular Per-Section) +- **Auto-registered** CIS Compliance Scanner MCP server — available out of the box +- **12 granular tools** instead of monolithic full-tenancy scan +- **Per-section data collection**: each scan tool collects only the OCI data needed for that section +- **Region-specific scanning**: all scan tools accept optional `regions` parameter +- **Session caching**: collected data cached per config+regions scope (2-hour TTL) +- **Parallelized data collection**: base and regional collectors run in parallel (up to 8 workers) + +### MCP Server Registry + Tool Discovery +- Register multiple MCP servers (stdio, SSE, Python module) +- **Automatic tool discovery**: connect to MCP servers and discover available tools +- **Chat Agent integration**: discovered tools are automatically available as GenAI function calls +- Upload `.py` scripts directly to servers +- **Link MCP servers to ADB Vector** databases as tools + +### Autonomous Database Vector Storage +- Oracle Autonomous Database connection with **mTLS Wallet** authentication +- `python-oracledb` Thin mode (no Oracle Client needed) +- **Multiple vector tables per ADB**: register, edit, toggle active/inactive +- **Multi-table RAG search**: queries all active tables across all ADB configs +- **Auto-resolve embedding config**: automatically uses existing OCI credentials for GenAI embeddings + +### Embeddings & Knowledge Base +- **Auto-embed CIS Reports**: one-click embedding of all report CSVs +- **CIS PDF Chunker**: segments CIS PDF by recommendation number, target 7000 chars with 500-char overlap +- **Auto-detect embedding dimension**: detects table dimension from DDL and selects correct model automatically +- **Knowledge Base**: upload documents (`.txt`, `.pdf`, `.csv`, `.json`, `.md`) or **import URLs** +- **Consult Embeddings**: chat-like interface with tenancy selector — queries vector store with CIS number detection +- 11 ADB vector tables supported (9 CIS report + 2 knowledge base) + +### User Management +- **Sub-menu**: Users, My Settings, Oracle IAM +- **My Settings**: per-user timezone, language preference (pt/en/es), password change (local users only), MFA toggle +- **Oracle IAM page**: OIDC configuration UI (issuer, client ID/secret, redirect URI, group-to-role mapping, test connection) +- **Users list**: admin view with role, status, MFA, auth provider badge (OIDC/Local) + +### Security +- **JWT authentication** with configurable expiry +- **TOTP MFA** (Google Authenticator / Authy compatible) +- **Oracle IAM OIDC**: SSO via Oracle Identity Domains — authorization code flow, JWKS signature validation, JIT user provisioning, group-to-role mapping +- **Dual auth modes**: local only, OIDC only, or both simultaneously +- **RBAC** with 3 roles: Admin, User, Viewer +- **Fernet encryption** (AES-128-CBC + HMAC-SHA256) for credentials and sensitive settings +- **User isolation**: ownership checks on ~70 endpoints, `is_global` flag for shared resources, private reports, per-user embeddings +- **Force password change** on first login (random password generated at startup) +- Audit logging for all operations +- Rate limiting on login and OIDC endpoints (10 attempts / 5 min) +- Non-root container execution + +### Theme & UI +- **Light/Dark mode** with Oracle Dark Premium design +- **KPI Dashboard**: compliance score gauge, pass/fail cards, donut chart, bar chart — powered by Recharts +- **20 pages**, code splitting, Zustand state persistence across navigation +- **i18n**: Portuguese, English, Spanish (850+ keys) + +--- + +## Deployment + +### Quick Setup (recommended) + +One command — handles authentication, pull, and container startup automatically: + +```bash +bash setup.sh +``` + +Output: +``` +======================================== + A-Team Security Agent — Setup +======================================== + +Authenticating... +Authenticated. +Pulling image... +Starting container on port 8080... + +======================================== + Setup Complete! +======================================== + + URL: http://localhost:8080 + User: admin + Password: admin123 + + You will be prompted to change the password on first login. +======================================== +``` + +Custom port: `PORT=9090 bash setup.sh` + +### Prerequisites + +- Docker installed +- Python 3 with `cryptography` package (auto-installed by setup.sh if missing) + +--- + +### Alternative: Manual Docker Run + +**Step 1 — Authenticate to OCIR** (one-time): + +```bash +python3 ocir-login.py --passphrase ateam-security-2026 +``` + +This passphrase is **read-only** — it only grants permission to pull the container image. It cannot be used to push images, access OCI resources, or modify any configuration. + +**Step 2 — Run the container**: + +```bash +docker run -d \ + --name oci-cis-agent \ + -p 8080:8080 \ + -v agent-data:/data \ + -e APP_SECRET=$(openssl rand -hex 64) \ + -e TZ=America/Sao_Paulo \ + --restart unless-stopped \ + us-ashburn-1.ocir.io/idi1o0a010nx/oci-cis-agent:latest +``` + +> If you get `403 Forbidden`, the OCIR login expired. Run `ocir-login.py` again. + +--- + +### Option 2 — OCI Compute Instance (Terraform) + +Automated production deployment on Oracle Cloud with Load Balancer, WAF, and SSL. + +```bash +cd terraform +cp terraform.tfvars.example terraform.tfvars +# Edit terraform.tfvars with OCI credentials + +terraform init +terraform plan +terraform apply +``` + +**Resources provisioned:** + +| Resource | Description | +|----------|-------------| +| VCN | 10.0.0.0/16 with public/private subnets, gateways, security lists | +| Compute | VM.Standard.A1.Flex — 2 OCPU, 16GB RAM (ARM, Free Tier eligible) | +| Block Volume | 50GB persistent storage for application data | +| Load Balancer | Flexible 10-100 Mbps with SSL | +| WAF | OWASP protection — XSS, SQL injection, path traversal + rate limiting | +| DNS | OCI DNS Zone + A record (conditional — when domain is configured) | + +Access: `https://` (from `terraform output app_url`) + +--- + +### Option 3 — OCI Container Instances + +Run as a serverless container on OCI without managing VMs. + +```bash +oci container-instances container-instance create \ + --compartment-id \ + --display-name "oci-cis-agent" \ + --availability-domain \ + --shape "CI.Standard.E4.Flex" \ + --shape-config '{"ocpus": 2, "memoryInGBs": 16}' \ + --containers '[{ + "imageUrl": "us-ashburn-1.ocir.io/idi1o0a010nx/oci-cis-agent:latest", + "displayName": "agent", + "environmentVariables": { + "APP_SECRET": "", + "TZ": "America/Sao_Paulo" + } + }]' \ + --vnics '[{"subnetId": ""}]' \ + --image-pull-secrets '[{ + "registryEndpoint": "us-ashburn-1.ocir.io", + "secretType": "BASIC", + "username": "", + "password": "" + }]' +``` + +--- + +### Option 4 — Kubernetes (OKE / any K8s cluster) + +Deploy on Oracle Kubernetes Engine or any Kubernetes cluster. + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: oci-cis-agent +spec: + replicas: 1 + selector: + matchLabels: + app: oci-cis-agent + template: + metadata: + labels: + app: oci-cis-agent + spec: + containers: + - name: agent + image: us-ashburn-1.ocir.io/idi1o0a010nx/oci-cis-agent:latest + ports: + - containerPort: 8080 + env: + - name: APP_SECRET + valueFrom: + secretKeyRef: + name: agent-secret + key: app-secret + - name: TZ + value: "America/Sao_Paulo" + volumeMounts: + - name: data + mountPath: /data + resources: + requests: + memory: "2Gi" + limits: + memory: "4Gi" + volumes: + - name: data + persistentVolumeClaim: + claimName: agent-data + imagePullSecrets: + - name: ocir-credentials +--- +apiVersion: v1 +kind: Service +metadata: + name: oci-cis-agent +spec: + type: LoadBalancer + ports: + - port: 443 + targetPort: 8080 + selector: + app: oci-cis-agent +``` + +--- + +### First Login + +After any deployment option, check the container logs for the initial admin password: + +```bash +docker logs oci-cis-agent | grep "password" +# or +kubectl logs deployment/oci-cis-agent | grep "password" +``` + +> You will be prompted to change the password on first login. + +--- + +## Configuration Guide + +### Step 1 — OCI Credentials + +Navigate to **OCI Credentials** tab and add: + +| Field | Description | +|-------|-------------| +| Tenancy Name | Friendly name (e.g., `my-company`) | +| OCID Tenancy | `ocid1.tenancy.oc1..xxxxx` | +| OCID User | `ocid1.user.oc1..xxxxx` | +| Fingerprint | `aa:bb:cc:dd:ee:ff:...` | +| Region | `sa-saopaulo-1`, `us-ashburn-1`, etc. | +| Compartment OCID | `ocid1.compartment.oc1..xxxxx` | +| Private Key | `.pem` file | +| Key Passphrase | *(optional)* Only required if the private key is encrypted | + +Click **Test** to validate the connection. + +### Step 2 — GenAI Model + +Navigate to **GenAI Config** tab: + +1. Select the **OCI Credential** created in Step 1 +2. Choose a **model** from the catalog (16 models across 5 providers) +3. Adjust the **GenAI region** if needed +4. Adjust parameters (temperature, max_tokens, etc.) + +For **dedicated endpoints**, switch Serving Type to `DEDICATED` and provide the endpoint ID. + +### Step 3 — MCP Servers (Optional) + +Register MCP servers for extended task execution and **Chat Agent tool use**: + +| Type | Use Case | +|------|----------| +| `stdio` | Local Python scripts (e.g., CIS check runner) | +| `SSE` | Remote HTTP servers | +| `module` | Upload `.py` files directly | + +**Tool Discovery**: After registering a server, click **"Discover Tools"** to automatically discover available tools via MCP protocol. Discovered tools are automatically available as **function calls** in the Chat Agent. + +### Step 4 — ADB Vector + RAG (Optional) + +For persistent vector storage and RAG-powered chat: + +1. Add DSN (TNS name from tnsnames.ora, e.g., `myatp_high`) +2. Set credentials (username/password) +3. Select an **Embedding Model** (Cohere Embed v4.0 recommended) +4. Upload Wallet ZIP (for mTLS) +5. Test the connection +6. **Register vector tables**: add the names of existing tables in your ADB + +> GenAI Config is optional — the app auto-resolves embedding credentials from your existing OCI config. + +#### Required ADB Vector Tables + +All tables must use the following schema: + +```sql +CREATE TABLE ( + ID RAW(16), + TEXT CLOB, + METADATA JSON, + EMBEDDING VECTOR +); +``` + +Run the SQL below to create all 11 required tables: + +```sql +-- CIS Report tables (auto-populated via "Embed Report") +CREATE TABLE summaryreportcsvvector (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +CREATE TABLE identityandaccess (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +CREATE TABLE networking (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +CREATE TABLE computeinstances (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +CREATE TABLE loggingandmonitoring (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +CREATE TABLE objectstorage (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +CREATE TABLE storageblockvolume (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +CREATE TABLE filestorageservice (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +CREATE TABLE assetmanagement (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); + +-- Knowledge base tables (populated manually via Embeddings tab) +CREATE TABLE cisrecom (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +CREATE TABLE engineerknowledgebase (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDDING VECTOR); +``` + +### Step 5 — Embeddings (Optional) + +Navigate to the **Embeddings** tab to populate the vector store: + +1. **CIS Recommendations**: Upload the CIS PDF to populate the `cisrecom` table +2. **Knowledge Base**: Upload documents or paste a URL to import web pages +3. **From CIS Reports**: Click "Embed Report" to auto-embed all findings CSVs +4. Browse and inspect embeddings per table + +Once embeddings exist, the **chat automatically uses RAG**. + +--- + +## OCI IAM Policies + +The following policies are required in your tenancy: + +``` +Allow group to use generative-ai-family in compartment +Allow group to read all-resources in tenancy +Allow group to inspect compartments in tenancy +Allow group to inspect autonomous-databases in compartment +Allow group to read virtual-network-family in compartment +Allow group to read instance-family in compartment +Allow group to read objectstorage-namespaces in tenancy +Allow group to read buckets in compartment +``` + +--- + +## Supported GenAI Models (16 Chat + 3 Embedding) + +### Chat Models + +| Provider | Model | Model ID | +|----------|-------|----------| +| Meta | Llama 4 Maverick | `meta.llama-4-maverick-17b-128e-instruct-fp8` | +| Meta | Llama 4 Scout | `meta.llama-4-scout-17b-16e-instruct` | +| Google | Gemini 2.5 Pro | `google.gemini-2.5-pro` | +| Google | Gemini 2.5 Flash | `google.gemini-2.5-flash` | +| OpenAI | GPT-5.2 | `openai.gpt-5.2` | +| OpenAI | GPT-5.1 | `openai.gpt-5.1` | +| OpenAI | GPT-5 Mini | `openai.gpt-5-mini` | +| OpenAI | **GPT-4.1 (Default)** | `openai.gpt-4.1` | +| OpenAI | GPT-4.1 Mini | `openai.gpt-4.1-mini` | +| OpenAI | GPT-4o | `openai.gpt-4o` | +| OpenAI | o4-mini | `openai.o4-mini` | +| OpenAI | o3 | `openai.o3` | +| xAI | Grok 4 | `xai.grok-4` | +| xAI | Grok 3 | `xai.grok-3` | +| xAI | Grok 3 Mini Fast | `xai.grok-3-mini-fast` | + +### Embedding Models + +| Provider | Model | Dimensions | +|----------|-------|------------| +| Cohere | Embed v4.0 (Multimodal) | 1536 | +| OpenAI | Text Embedding 3 Large | 3072 | +| OpenAI | Text Embedding 3 Small | 1536 | + +--- + +## Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `APP_SECRET` | **Yes** | — | 64-byte hex key for JWT/encryption (`openssl rand -hex 64`) | +| `JWT_EXPIRY_HOURS` | No | `12` | Token expiry in hours | +| `CORS_ORIGINS` | No | — | Allowed origins (comma-separated) | +| `TZ` | No | `America/Sao_Paulo` | Timezone | + +--- + +## Troubleshooting + +**Cannot pull images from OCIR:** +Verify your `docker login` credentials and that the OCIR repository exists in your namespace. + +**Backend health check fails:** +Check logs: `docker logs oci-cis-agent`. Ensure `APP_SECRET` is set. + +**ADB connection fails (`DPY-6005`):** +Ensure the wallet ZIP contains `tnsnames.ora` and `ewallet.pem`. The DSN must match a service name in `tnsnames.ora`. + +**GenAI returns 401/403:** +Verify the IAM policy `Allow group ... to use generative-ai-family in compartment ...` exists. + +**OCI CLI test hangs with passphrase prompt:** +If your private key is encrypted, provide the passphrase in the **Key Passphrase** field when saving the credential. + +--- + +## License + +MIT + +--- + +

+ Built for Oracle Cloud Infrastructure security compliance by LAD A-Team +

diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..0b7b905 --- /dev/null +++ b/logo.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/ocir-login.py b/ocir-login.py new file mode 100755 index 0000000..92fa6a4 --- /dev/null +++ b/ocir-login.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +""" +OCIR Login Helper — Decrypts embedded credentials and performs docker login. + +The auth token is encrypted with Fernet (AES-128-CBC + HMAC-SHA256). +A passphrase is required to decrypt — distribute it out-of-band (Slack, email). + +Usage: + python ocir-login.py + python ocir-login.py --passphrase + python ocir-login.py --generate # Admin: encrypt a new token +""" +import argparse +import base64 +import getpass +import hashlib +import os +import subprocess +import sys + +# ── OCIR Configuration ───────────────────────────────────────────────────── +OCIR_REGION = "us-ashburn-1" +OCIR_ENDPOINT = f"{OCIR_REGION}.ocir.io" +OCIR_NAMESPACE = "idi1o0a010nx" +OCIR_USERNAME = f"{OCIR_NAMESPACE}/oracleidentitycloudservice/gustavo.nogueira@oracle.com" + +# ── Encrypted Token (generated with --generate) ──────────────────────────── +ENCRYPTED_TOKEN = "gAAAAABp0CYOb2bj1neOKUS8L_OOaNTcqEcgWouGiHtn96LDoOgpzJ1YeITNSq5tOqJZRW2JcEMZRWsbC3rc0tAoGsvM2o_Fh_q4RIoyP6nzJ0B1JLBRqnk=" + +# ── Salt for key derivation (fixed, not secret) ──────────────────────────── +_SALT = b"oci-cis-agent-ocir-v1" + + +def _derive_key(passphrase: str) -> bytes: + """Derive Fernet key from passphrase using PBKDF2.""" + from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + from cryptography.hazmat.primitives import hashes + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=_SALT, + iterations=600_000, + ) + return base64.urlsafe_b64encode(kdf.derive(passphrase.encode())) + + +def encrypt_token(token: str, passphrase: str) -> str: + """Encrypt a token with a passphrase. Returns base64 string.""" + from cryptography.fernet import Fernet + key = _derive_key(passphrase) + f = Fernet(key) + return f.encrypt(token.encode()).decode() + + +def decrypt_token(encrypted: str, passphrase: str) -> str: + """Decrypt a token with a passphrase.""" + from cryptography.fernet import Fernet, InvalidToken + key = _derive_key(passphrase) + f = Fernet(key) + try: + return f.decrypt(encrypted.encode()).decode() + except InvalidToken: + return "" + + +def docker_login(token: str) -> bool: + """Perform docker login to OCIR.""" + result = subprocess.run( + ["docker", "login", OCIR_ENDPOINT, "-u", OCIR_USERNAME, "--password-stdin"], + input=token, + capture_output=True, + text=True, + ) + if result.returncode == 0: + print(f"Login succeeded: {OCIR_ENDPOINT}") + return True + else: + print(f"Login failed: {result.stderr.strip()}") + return False + + +def main(): + parser = argparse.ArgumentParser(description="OCIR Login Helper") + parser.add_argument("--passphrase", help="Passphrase to decrypt the auth token") + parser.add_argument("--generate", action="store_true", help="Admin: encrypt a new auth token") + parser.add_argument("--pull", action="store_true", help="Also pull the image after login") + args = parser.parse_args() + + if args.generate: + print("=== Generate Encrypted Token ===") + token = getpass.getpass("Auth Token (from OCI Console): ") + passphrase = getpass.getpass("Passphrase (share with team): ") + passphrase2 = getpass.getpass("Confirm passphrase: ") + if passphrase != passphrase2: + print("Passphrases do not match.") + sys.exit(1) + encrypted = encrypt_token(token, passphrase) + print(f"\nEncrypted token (copy to ENCRYPTED_TOKEN in this script):\n") + print(f'ENCRYPTED_TOKEN = "{encrypted}"') + print(f"\nPassphrase to share: {passphrase}") + # Verify + decrypted = decrypt_token(encrypted, passphrase) + assert decrypted == token, "Verification failed!" + print("Verification: OK") + return + + if not ENCRYPTED_TOKEN: + print("No encrypted token configured. Run with --generate first.") + sys.exit(1) + + passphrase = args.passphrase or getpass.getpass("Passphrase: ") + token = decrypt_token(ENCRYPTED_TOKEN, passphrase) + + if not token: + print("Invalid passphrase.") + sys.exit(1) + + success = docker_login(token) + + if success and args.pull: + image = f"{OCIR_ENDPOINT}/{OCIR_NAMESPACE}/oci-cis-agent:latest" + print(f"\nPulling {image}...") + subprocess.run(["docker", "pull", image]) + + # Clear sensitive data + token = None + passphrase = None + + +if __name__ == "__main__": + main() diff --git a/setup.sh b/setup.sh new file mode 100755 index 0000000..ffb85a9 --- /dev/null +++ b/setup.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# ============================================================================ +# A-Team Security — Infrastructure & Security Agent Engineer +# One-command setup: login → pull → run +# ============================================================================ +set -euo pipefail + +IMAGE="us-ashburn-1.ocir.io/idi1o0a010nx/oci-cis-agent:latest" +CONTAINER_NAME="oci-cis-agent" +PORT="${PORT:-8080}" + +# ── Embedded credentials (read-only, scoped to this image only) ──────────── +_R="us-ashburn-1.ocir.io" +_U="idi1o0a010nx/oracleidentitycloudservice/gustavo.nogueira@oracle.com" +_K() { python3 -c " +import base64 as b,hashlib as h +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives import hashes +from cryptography.fernet import Fernet +k=PBKDF2HMAC(algorithm=hashes.SHA256(),length=32,salt=b'oci-cis-agent-ocir-v1',iterations=600000) +f=Fernet(b.urlsafe_b64encode(k.derive(b'$(printf '%s' "YXRlYW0tc2VjdXJpdHktMjAyNg==" | base64 -d)'))) +print(f.decrypt(b'gAAAAABp0CYOb2bj1neOKUS8L_OOaNTcqEcgWouGiHtn96LDoOgpzJ1YeITNSq5tOqJZRW2JcEMZRWsbC3rc0tAoGsvM2o_Fh_q4RIoyP6nzJ0B1JLBRqnk=').decode(),end='') +"; } + +echo "========================================" +echo " A-Team Security Agent — Setup" +echo "========================================" +echo "" + +# ── Check dependencies ───────────────────────────────────────────────────── +for cmd in docker python3; do + if ! command -v $cmd &>/dev/null; then + echo "Error: $cmd is required but not installed." + exit 1 + fi +done + +python3 -c "from cryptography.fernet import Fernet" 2>/dev/null || { + echo "Installing cryptography..." + pip3 install cryptography --quiet +} + +# ── Stop existing container if running ───────────────────────────────────── +if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Stopping existing container..." + docker stop "$CONTAINER_NAME" 2>/dev/null || true + docker rm "$CONTAINER_NAME" 2>/dev/null || true +fi + +# ── Login to OCIR ────────────────────────────────────────────────────────── +echo "Authenticating..." +_K | docker login "$_R" -u "$_U" --password-stdin >/dev/null 2>&1 +echo "Authenticated." + +# ── Pull latest image ────────────────────────────────────────────────────── +echo "Pulling image..." +docker pull "$IMAGE" + +# ── Generate APP_SECRET if not set ───────────────────────────────────────── +if [ -z "${APP_SECRET:-}" ]; then + APP_SECRET=$(openssl rand -hex 64) + echo "Generated APP_SECRET." +fi + +# ── Run container ────────────────────────────────────────────────────────── +echo "Starting container on port $PORT..." +docker run -d \ + --name "$CONTAINER_NAME" \ + -p "${PORT}:8080" \ + -v agent-data:/data \ + -e APP_SECRET="$APP_SECRET" \ + -e TZ="${TZ:-America/Sao_Paulo}" \ + --restart unless-stopped \ + "$IMAGE" + +# ── Wait for startup ────────────────────────────────────────────────────── +echo "Waiting for application to start..." +for i in $(seq 1 30); do + if curl -sf "http://localhost:${PORT}/api/health" >/dev/null 2>&1; then + break + fi + sleep 2 +done + +# ── Show access info ─────────────────────────────────────────────────────── +echo "" +echo "========================================" +echo " Setup Complete!" +echo "========================================" +echo "" +echo " URL: http://localhost:${PORT}" +echo " User: admin" +echo " Password: $(docker logs "$CONTAINER_NAME" 2>&1 | grep 'password:' | head -1 | sed 's/.*password: //')" +echo "" +echo " You will be prompted to change the password on first login." +echo "" +echo "========================================" diff --git a/terraform/compute.tf b/terraform/compute.tf new file mode 100644 index 0000000..fb12833 --- /dev/null +++ b/terraform/compute.tf @@ -0,0 +1,78 @@ +# ----------------------------------------------------------------------------- +# Data Sources +# ----------------------------------------------------------------------------- +data "oci_identity_availability_domains" "ads" { + compartment_id = var.tenancy_ocid +} + +data "oci_core_images" "oracle_linux_8" { + compartment_id = var.compartment_ocid + operating_system = "Oracle Linux" + operating_system_version = "8" + shape = var.instance_shape + sort_by = "TIMECREATED" + sort_order = "DESC" + + filter { + name = "display_name" + values = ["^Oracle-Linux-8\\..*-aarch64-.*$"] + regex = true + } +} + +# ----------------------------------------------------------------------------- +# Compute Instance +# ----------------------------------------------------------------------------- +resource "oci_core_instance" "app" { + compartment_id = var.compartment_ocid + availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name + display_name = "cisagent-vm" + shape = var.instance_shape + freeform_tags = var.freeform_tags + + shape_config { + ocpus = var.instance_ocpus + memory_in_gbs = var.instance_memory_gb + } + + source_details { + source_type = "image" + source_id = data.oci_core_images.oracle_linux_8.images[0].id + } + + create_vnic_details { + subnet_id = oci_core_subnet.private.id + assign_public_ip = false + display_name = "cisagent-vnic" + hostname_label = "cisagent" + } + + metadata = { + ssh_authorized_keys = var.ssh_public_key + user_data = base64encode(templatefile("${path.module}/templates/cloud-init.sh.tpl", { + ocir_region = var.region + ocir_namespace = var.ocir_namespace + ocir_username = var.ocir_username + ocir_auth_token = var.ocir_auth_token + ocir_repo_prefix = var.ocir_repo_prefix + app_secret = var.app_secret + jwt_expiry_hours = var.jwt_expiry_hours + app_timezone = var.app_timezone + cors_origins = var.domain_name != "" ? "https://${var.domain_name}" : "*" + data_device = "/dev/oracleoci/oraclevdb" + })) + } + + agent_config { + is_monitoring_disabled = false + is_management_disabled = false + are_all_plugins_disabled = false + } + + lifecycle { + ignore_changes = [ + source_details[0].source_id, + metadata["user_data"], + ] + } +} diff --git a/terraform/dns.tf b/terraform/dns.tf new file mode 100644 index 0000000..cef7882 --- /dev/null +++ b/terraform/dns.tf @@ -0,0 +1,36 @@ +# ----------------------------------------------------------------------------- +# DNS Zone and A Record (conditional — only when domain_name is set) +# ----------------------------------------------------------------------------- + +resource "oci_dns_zone" "main" { + count = var.domain_name != "" ? 1 : 0 + compartment_id = var.compartment_ocid + name = var.domain_name + zone_type = "PRIMARY" + freeform_tags = var.freeform_tags +} + +resource "oci_dns_rrset" "a_record" { + count = var.domain_name != "" ? 1 : 0 + zone_name_or_id = oci_dns_zone.main[0].id + domain = var.domain_name + rtype = "A" + compartment_id = var.compartment_ocid + + items { + domain = var.domain_name + rtype = "A" + rdata = oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address + ttl = 300 + } +} + +# TODO: Add OCI Certificates Service integration for Let's Encrypt when domain +# is validated. This requires: +# 1. oci_certificates_management_certificate_authority (subordinate CA) +# 2. oci_certificates_management_certificate with ISSUED_BY_INTERNAL_CA config +# 3. DNS-01 challenge validation via the OCI DNS zone above +# 4. Update LB listener SSL config to use the managed certificate +# For now, the self-signed cert is used even when a domain is configured. +# After DNS propagation, you can manually configure Let's Encrypt via certbot +# or the OCI Certificates Service console. diff --git a/terraform/lb.tf b/terraform/lb.tf new file mode 100644 index 0000000..8202b78 --- /dev/null +++ b/terraform/lb.tf @@ -0,0 +1,143 @@ +# ----------------------------------------------------------------------------- +# Self-Signed TLS Certificate (fallback when no domain) +# ----------------------------------------------------------------------------- +resource "tls_private_key" "lb" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "lb" { + private_key_pem = tls_private_key.lb.private_key_pem + + subject { + common_name = var.domain_name != "" ? var.domain_name : "cisagent.local" + organization = "OCI CIS Agent" + } + + validity_period_hours = 8760 # 1 year + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth", + ] +} + +# ----------------------------------------------------------------------------- +# Load Balancer +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_load_balancer" "main" { + compartment_id = var.compartment_ocid + display_name = "cisagent-lb" + shape = "flexible" + freeform_tags = var.freeform_tags + + shape_details { + minimum_bandwidth_in_mbps = var.lb_min_bandwidth_mbps + maximum_bandwidth_in_mbps = var.lb_max_bandwidth_mbps + } + + subnet_ids = [oci_core_subnet.public.id] + + is_private = false +} + +# ----------------------------------------------------------------------------- +# Certificate +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_certificate" "self_signed" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + certificate_name = "cisagent-self-signed" + public_certificate = tls_self_signed_cert.lb.cert_pem + private_key = tls_private_key.lb.private_key_pem + + lifecycle { + create_before_destroy = true + } +} + +# ----------------------------------------------------------------------------- +# Backend Set +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_backend_set" "app" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "cisagent-backend-set" + policy = "ROUND_ROBIN" + + health_checker { + protocol = "HTTP" + port = 8080 + url_path = "/api/health" + interval_ms = 10000 + timeout_in_millis = 5000 + retries = 3 + return_code = 200 + } +} + +# ----------------------------------------------------------------------------- +# Backend +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_backend" "app" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + backendset_name = oci_load_balancer_backend_set.app.name + ip_address = oci_core_instance.app.private_ip + port = 8080 +} + +# ----------------------------------------------------------------------------- +# HTTPS Listener (443) +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_listener" "https" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "cisagent-https-listener" + default_backend_set_name = oci_load_balancer_backend_set.app.name + port = 443 + protocol = "HTTP" + + ssl_configuration { + certificate_name = oci_load_balancer_certificate.self_signed.certificate_name + verify_peer_certificate = false + protocols = ["TLSv1.2", "TLSv1.3"] + } +} + +# ----------------------------------------------------------------------------- +# HTTP → HTTPS Redirect Rule Set +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_rule_set" "redirect_http_to_https" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "redirect-http-to-https" + + items { + action = "REDIRECT" + + conditions { + attribute_name = "PATH" + attribute_value = "/" + operator = "FORCE_LONGEST_PREFIX_MATCH" + } + + redirect_uri { + protocol = "HTTPS" + port = 443 + host = "{host}" + path = "/{path}" + query = "?{query}" + } + + response_code = 301 + } +} + +# ----------------------------------------------------------------------------- +# HTTP Listener (80) with Redirect +# ----------------------------------------------------------------------------- +resource "oci_load_balancer_listener" "http" { + load_balancer_id = oci_load_balancer_load_balancer.main.id + name = "cisagent-http-listener" + default_backend_set_name = oci_load_balancer_backend_set.app.name + port = 80 + protocol = "HTTP" + rule_set_names = [oci_load_balancer_rule_set.redirect_http_to_https.name] +} diff --git a/terraform/network.tf b/terraform/network.tf new file mode 100644 index 0000000..5ceb520 --- /dev/null +++ b/terraform/network.tf @@ -0,0 +1,189 @@ +# ----------------------------------------------------------------------------- +# VCN +# ----------------------------------------------------------------------------- +resource "oci_core_vcn" "main" { + compartment_id = var.compartment_ocid + cidr_blocks = [var.vcn_cidr] + display_name = "cisagent-vcn" + dns_label = "cisagent" + freeform_tags = var.freeform_tags +} + +# ----------------------------------------------------------------------------- +# Gateways +# ----------------------------------------------------------------------------- +resource "oci_core_internet_gateway" "main" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-igw" + enabled = true + freeform_tags = var.freeform_tags +} + +resource "oci_core_nat_gateway" "main" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-natgw" + freeform_tags = var.freeform_tags +} + +data "oci_core_services" "all" { + filter { + name = "name" + values = ["All .* Services In Oracle Services Network"] + regex = true + } +} + +resource "oci_core_service_gateway" "main" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-svcgw" + + services { + service_id = data.oci_core_services.all.services[0].id + } + + freeform_tags = var.freeform_tags +} + +# ----------------------------------------------------------------------------- +# Route Tables +# ----------------------------------------------------------------------------- +resource "oci_core_route_table" "public" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-public-rt" + freeform_tags = var.freeform_tags + + route_rules { + network_entity_id = oci_core_internet_gateway.main.id + destination = "0.0.0.0/0" + destination_type = "CIDR_BLOCK" + } +} + +resource "oci_core_route_table" "private" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-private-rt" + freeform_tags = var.freeform_tags + + route_rules { + network_entity_id = oci_core_nat_gateway.main.id + destination = "0.0.0.0/0" + destination_type = "CIDR_BLOCK" + } + + route_rules { + network_entity_id = oci_core_service_gateway.main.id + destination = data.oci_core_services.all.services[0].cidr_block + destination_type = "SERVICE_CIDR_BLOCK" + } +} + +# ----------------------------------------------------------------------------- +# Security Lists +# ----------------------------------------------------------------------------- +resource "oci_core_security_list" "public" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-public-sl" + freeform_tags = var.freeform_tags + + # Ingress: HTTP + ingress_security_rules { + protocol = "6" # TCP + source = "0.0.0.0/0" + stateless = false + + tcp_options { + min = 80 + max = 80 + } + } + + # Ingress: HTTPS + ingress_security_rules { + protocol = "6" + source = "0.0.0.0/0" + stateless = false + + tcp_options { + min = 443 + max = 443 + } + } + + # Egress: All + egress_security_rules { + protocol = "all" + destination = "0.0.0.0/0" + stateless = false + } +} + +resource "oci_core_security_list" "private" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + display_name = "cisagent-private-sl" + freeform_tags = var.freeform_tags + + # Ingress: LB health checks and traffic from public subnet + ingress_security_rules { + protocol = "6" + source = var.public_subnet_cidr + stateless = false + + tcp_options { + min = 8080 + max = 8080 + } + } + + # Ingress: SSH from within VCN + ingress_security_rules { + protocol = "6" + source = var.vcn_cidr + stateless = false + + tcp_options { + min = 22 + max = 22 + } + } + + # Egress: All + egress_security_rules { + protocol = "all" + destination = "0.0.0.0/0" + stateless = false + } +} + +# ----------------------------------------------------------------------------- +# Subnets +# ----------------------------------------------------------------------------- +resource "oci_core_subnet" "public" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + cidr_block = var.public_subnet_cidr + display_name = "cisagent-public-subnet" + dns_label = "pub" + route_table_id = oci_core_route_table.public.id + security_list_ids = [oci_core_security_list.public.id] + prohibit_public_ip_on_vnic = false + freeform_tags = var.freeform_tags +} + +resource "oci_core_subnet" "private" { + compartment_id = var.compartment_ocid + vcn_id = oci_core_vcn.main.id + cidr_block = var.private_subnet_cidr + display_name = "cisagent-private-subnet" + dns_label = "priv" + route_table_id = oci_core_route_table.private.id + security_list_ids = [oci_core_security_list.private.id] + prohibit_public_ip_on_vnic = true + freeform_tags = var.freeform_tags +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..70d7aa3 --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,56 @@ +# ----------------------------------------------------------------------------- +# Outputs +# ----------------------------------------------------------------------------- + +output "load_balancer_ip" { + description = "Public IP address of the load balancer" + value = oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address +} + +output "app_url" { + description = "Application URL" + value = var.domain_name != "" ? "https://${var.domain_name}" : "https://${oci_load_balancer_load_balancer.main.ip_address_details[0].ip_address}" +} + +output "instance_private_ip" { + description = "Private IP of the compute instance" + value = oci_core_instance.app.private_ip +} + +output "instance_ocid" { + description = "OCID of the compute instance" + value = oci_core_instance.app.id +} + +output "ocir_backend_url" { + description = "OCIR URL for the backend image" + value = "${local.ocir_endpoint}/${var.ocir_namespace}/${var.ocir_repo_prefix}/backend" +} + +output "ocir_frontend_url" { + description = "OCIR URL for the frontend image" + value = "${local.ocir_endpoint}/${var.ocir_namespace}/${var.ocir_repo_prefix}/frontend" +} + +output "ssh_command" { + description = "SSH command to connect to the instance (requires bastion or VPN)" + value = "ssh opc@${oci_core_instance.app.private_ip}" +} + +output "docker_login_command" { + description = "Docker login command for OCIR" + value = "docker login ${local.ocir_endpoint} -u '${var.ocir_namespace}/${var.ocir_username}'" + sensitive = false +} + +output "dns_nameservers" { + description = "DNS nameservers for the zone (only when domain_name is set)" + value = var.domain_name != "" ? oci_dns_zone.main[0].nameservers[*].hostname : [] +} + +# ----------------------------------------------------------------------------- +# Locals +# ----------------------------------------------------------------------------- +locals { + ocir_endpoint = "${var.region}.ocir.io" +} diff --git a/terraform/provider.tf b/terraform/provider.tf new file mode 100644 index 0000000..0b8d9ba --- /dev/null +++ b/terraform/provider.tf @@ -0,0 +1,22 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + oci = { + source = "oracle/oci" + version = ">= 6.0.0" + } + tls = { + source = "hashicorp/tls" + version = ">= 4.0.0" + } + } +} + +provider "oci" { + tenancy_ocid = var.tenancy_ocid + user_ocid = var.user_ocid + fingerprint = var.fingerprint + private_key_path = var.private_key_path + region = var.region +} diff --git a/terraform/storage.tf b/terraform/storage.tf new file mode 100644 index 0000000..f9ac9d9 --- /dev/null +++ b/terraform/storage.tf @@ -0,0 +1,22 @@ +# ----------------------------------------------------------------------------- +# Block Volume for /data persistence +# ----------------------------------------------------------------------------- +resource "oci_core_volume" "data" { + compartment_id = var.compartment_ocid + availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name + display_name = "cisagent-data-vol" + size_in_gbs = var.block_volume_size_gb + vpus_per_gb = var.block_volume_vpus_per_gb + freeform_tags = var.freeform_tags +} + +resource "oci_core_volume_attachment" "data" { + attachment_type = "paravirtualized" + instance_id = oci_core_instance.app.id + volume_id = oci_core_volume.data.id + display_name = "cisagent-data-vol-attach" + device = "/dev/oracleoci/oraclevdb" + + # Ensure instance is fully running before attaching + depends_on = [oci_core_instance.app] +} diff --git a/terraform/templates/cloud-init.sh.tpl b/terraform/templates/cloud-init.sh.tpl new file mode 100644 index 0000000..1ff1316 --- /dev/null +++ b/terraform/templates/cloud-init.sh.tpl @@ -0,0 +1,141 @@ +#!/bin/bash +set -euo pipefail + +exec > >(tee /var/log/cloud-init-app.log) 2>&1 +echo "=== OCI CIS Agent cloud-init started at $(date) ===" + +# ----------------------------------------------------------------------------- +# 1. Install Docker CE + docker-compose-plugin +# ----------------------------------------------------------------------------- +echo ">>> Installing Docker CE..." +dnf install -y dnf-utils +dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo +dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin --allowerasing + +systemctl enable docker +systemctl start docker + +echo ">>> Docker version: $(docker --version)" +echo ">>> Docker Compose version: $(docker compose version)" + +# ----------------------------------------------------------------------------- +# 2. Wait for block device +# ----------------------------------------------------------------------------- +DATA_DEVICE="${data_device}" +echo ">>> Waiting for block device $DATA_DEVICE..." +ATTEMPTS=0 +MAX_ATTEMPTS=60 +while [ ! -e "$DATA_DEVICE" ] && [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do + echo " Waiting for $DATA_DEVICE (attempt $((ATTEMPTS+1))/$MAX_ATTEMPTS)..." + sleep 5 + ATTEMPTS=$((ATTEMPTS+1)) +done + +if [ ! -e "$DATA_DEVICE" ]; then + echo "ERROR: Block device $DATA_DEVICE not found after $MAX_ATTEMPTS attempts" + exit 1 +fi +echo ">>> Block device $DATA_DEVICE is available" + +# ----------------------------------------------------------------------------- +# 3. Format and mount /data +# ----------------------------------------------------------------------------- +if ! blkid "$DATA_DEVICE" | grep -q ext4; then + echo ">>> Formatting $DATA_DEVICE as ext4..." + mkfs.ext4 -F "$DATA_DEVICE" +fi + +mkdir -p /data +mount "$DATA_DEVICE" /data + +if ! grep -q "$DATA_DEVICE" /etc/fstab; then + echo "$DATA_DEVICE /data ext4 defaults,_netdev,nofail 0 2" >> /etc/fstab +fi + +echo ">>> /data mounted successfully" +df -h /data + +# ----------------------------------------------------------------------------- +# 4. Docker login to OCIR +# ----------------------------------------------------------------------------- +OCIR_ENDPOINT="${ocir_region}.ocir.io" +echo ">>> Logging into OCIR at $OCIR_ENDPOINT..." +echo '${ocir_auth_token}' | docker login "$OCIR_ENDPOINT" \ + -u "${ocir_namespace}/${ocir_username}" \ + --password-stdin + +# ----------------------------------------------------------------------------- +# 5. Pull image +# ----------------------------------------------------------------------------- +IMAGE="$OCIR_ENDPOINT/${ocir_namespace}/${ocir_repo_prefix}:latest" + +echo ">>> Pulling image: $IMAGE" +docker pull "$IMAGE" + +# ----------------------------------------------------------------------------- +# 6. Write docker-compose.yml +# ----------------------------------------------------------------------------- +APP_DIR="/opt/oci-cis-agent" +mkdir -p "$APP_DIR" + +cat > "$APP_DIR/docker-compose.yml" <<'COMPOSE_EOF' +services: + agent: + image: ${ocir_region}.ocir.io/${ocir_namespace}/${ocir_repo_prefix}:latest + container_name: oci-cis-agent + restart: unless-stopped + environment: + - APP_SECRET=${app_secret} + - JWT_EXPIRY_HOURS=${jwt_expiry_hours} + - DATA_DIR=/data + - CORS_ORIGINS=${cors_origins} + - TZ=${app_timezone} + - OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True + ports: + - "8080:8080" + volumes: + - /data:/data + mem_limit: 4g + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + +COMPOSE_EOF + +echo ">>> docker-compose.yml written to $APP_DIR" + +# ----------------------------------------------------------------------------- +# 7. Start application +# ----------------------------------------------------------------------------- +echo ">>> Starting application..." +cd "$APP_DIR" +docker compose up -d + +# ----------------------------------------------------------------------------- +# 8. Health check wait loop +# ----------------------------------------------------------------------------- +echo ">>> Waiting for application to become healthy..." +ATTEMPTS=0 +MAX_ATTEMPTS=60 +while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do + if curl -sf http://localhost:8080/api/health > /dev/null 2>&1; then + echo ">>> Application is healthy!" + break + fi + echo " Health check attempt $((ATTEMPTS+1))/$MAX_ATTEMPTS..." + sleep 10 + ATTEMPTS=$((ATTEMPTS+1)) +done + +if [ $ATTEMPTS -eq $MAX_ATTEMPTS ]; then + echo "WARNING: Application did not become healthy within expected time" + echo ">>> Container status:" + docker compose ps + echo ">>> Logs:" + docker compose logs --tail 50 +fi + +echo "=== OCI CIS Agent cloud-init finished at $(date) ===" diff --git a/terraform/terraform.tfvars.example b/terraform/terraform.tfvars.example new file mode 100644 index 0000000..40f41e2 --- /dev/null +++ b/terraform/terraform.tfvars.example @@ -0,0 +1,55 @@ +# ============================================================================= +# OCI CIS Agent — VM Deployment Variables +# ============================================================================= +# Copy this file to terraform.tfvars and fill in the values. +# Required values are marked with . +# ============================================================================= + +# --- OCI Authentication --- +tenancy_ocid = "" +user_ocid = "" +fingerprint = "" +private_key_path = "" # e.g. "~/.oci/oci_api_key.pem" +region = "us-ashburn-1" + +# --- Compartment --- +compartment_ocid = "" + +# --- Networking (defaults are usually fine) --- +# vcn_cidr = "10.0.0.0/16" +# public_subnet_cidr = "10.0.1.0/24" +# private_subnet_cidr = "10.0.2.0/24" + +# --- Compute Instance --- +# instance_shape = "VM.Standard.A1.Flex" +# instance_ocpus = 2 +# instance_memory_gb = 16 +ssh_public_key = "" # contents of ~/.ssh/id_rsa.pub + +# --- Block Volume --- +# block_volume_size_gb = 50 +# block_volume_vpus_per_gb = 20 + +# --- Application --- +app_secret = "" # generate with: python3 -c "import secrets; print(secrets.token_hex(64))" +# jwt_expiry_hours = 12 +# app_timezone = "UTC" + +# --- OCIR --- +ocir_namespace = "" # tenancy Object Storage namespace +ocir_username = "" # e.g. "user@email.com" (without namespace prefix) +ocir_auth_token = "" # generate in OCI Console > User Settings > Auth Tokens +# ocir_repo_prefix = "oci-cis-agent" + +# --- Load Balancer --- +# lb_min_bandwidth_mbps = 10 +# lb_max_bandwidth_mbps = 100 + +# --- DNS (optional — leave empty to skip) --- +# domain_name = "" + +# --- Tags --- +# freeform_tags = { +# "Project" = "oci-cis-agent" +# "ManagedBy" = "terraform" +# } diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..86d911c --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,185 @@ +# ----------------------------------------------------------------------------- +# OCI Authentication +# ----------------------------------------------------------------------------- +variable "tenancy_ocid" { + description = "OCID of the OCI tenancy" + type = string +} + +variable "user_ocid" { + description = "OCID of the OCI user for API authentication" + type = string +} + +variable "fingerprint" { + description = "Fingerprint of the OCI API signing key" + type = string +} + +variable "private_key_path" { + description = "Path to the OCI API private key PEM file" + type = string +} + +variable "region" { + description = "OCI region for deployment" + type = string + default = "us-ashburn-1" +} + +# ----------------------------------------------------------------------------- +# Compartment +# ----------------------------------------------------------------------------- +variable "compartment_ocid" { + description = "OCID of the existing compartment for all resources" + type = string +} + +# ----------------------------------------------------------------------------- +# Networking +# ----------------------------------------------------------------------------- +variable "vcn_cidr" { + description = "CIDR block for the VCN" + type = string + default = "10.0.0.0/16" +} + +variable "public_subnet_cidr" { + description = "CIDR block for the public subnet (Load Balancer)" + type = string + default = "10.0.1.0/24" +} + +variable "private_subnet_cidr" { + description = "CIDR block for the private subnet (Compute)" + type = string + default = "10.0.2.0/24" +} + +# ----------------------------------------------------------------------------- +# Compute Instance +# ----------------------------------------------------------------------------- +variable "instance_shape" { + description = "Compute instance shape" + type = string + default = "VM.Standard.A1.Flex" +} + +variable "instance_ocpus" { + description = "Number of OCPUs for the flex instance" + type = number + default = 2 +} + +variable "instance_memory_gb" { + description = "Memory in GB for the flex instance" + type = number + default = 16 +} + +variable "ssh_public_key" { + description = "SSH public key for instance access" + type = string +} + +# ----------------------------------------------------------------------------- +# Block Volume +# ----------------------------------------------------------------------------- +variable "block_volume_size_gb" { + description = "Size of the block volume in GB for /data" + type = number + default = 50 +} + +variable "block_volume_vpus_per_gb" { + description = "Volume performance units per GB (10=Balanced, 20=Higher, 30+=Ultra High)" + type = number + default = 20 +} + +# ----------------------------------------------------------------------------- +# Application Configuration +# ----------------------------------------------------------------------------- +variable "app_secret" { + description = "Application secret key (64-byte hex string, 128 characters)" + type = string + sensitive = true + + validation { + condition = can(regex("^[0-9a-fA-F]{128}$", var.app_secret)) + error_message = "app_secret must be a 128-character hexadecimal string (64 bytes)." + } +} + +variable "jwt_expiry_hours" { + description = "JWT token expiry time in hours" + type = number + default = 12 +} + +variable "app_timezone" { + description = "Timezone for the application (e.g. America/Sao_Paulo)" + type = string + default = "UTC" +} + +# ----------------------------------------------------------------------------- +# OCIR (Oracle Cloud Infrastructure Registry) +# ----------------------------------------------------------------------------- +variable "ocir_namespace" { + description = "Object Storage namespace for OCIR (tenancy namespace)" + type = string +} + +variable "ocir_username" { + description = "Username for OCIR login (e.g. tenancy-namespace/user@email.com)" + type = string +} + +variable "ocir_auth_token" { + description = "Auth token for OCIR login" + type = string + sensitive = true +} + +variable "ocir_repo_prefix" { + description = "Repository prefix in OCIR (e.g. oci-cis-agent)" + type = string + default = "oci-cis-agent" +} + +# ----------------------------------------------------------------------------- +# Load Balancer +# ----------------------------------------------------------------------------- +variable "lb_min_bandwidth_mbps" { + description = "Minimum bandwidth for the flexible load balancer in Mbps" + type = number + default = 10 +} + +variable "lb_max_bandwidth_mbps" { + description = "Maximum bandwidth for the flexible load balancer in Mbps" + type = number + default = 100 +} + +# ----------------------------------------------------------------------------- +# DNS (Optional) +# ----------------------------------------------------------------------------- +variable "domain_name" { + description = "Domain name for DNS zone and A record. Leave empty to skip DNS setup." + type = string + default = "" +} + +# ----------------------------------------------------------------------------- +# Tags +# ----------------------------------------------------------------------------- +variable "freeform_tags" { + description = "Freeform tags to apply to all resources" + type = map(string) + default = { + "Project" = "oci-cis-agent" + "ManagedBy" = "terraform" + } +} diff --git a/terraform/waf.tf b/terraform/waf.tf new file mode 100644 index 0000000..c9d2d91 --- /dev/null +++ b/terraform/waf.tf @@ -0,0 +1,120 @@ +# ----------------------------------------------------------------------------- +# WAF Policy +# ----------------------------------------------------------------------------- +resource "oci_waf_web_app_firewall_policy" "main" { + compartment_id = var.compartment_ocid + display_name = "cisagent-waf-policy" + freeform_tags = var.freeform_tags + + # --- Actions --- + actions { + name = "checkAction" + type = "CHECK" + } + + actions { + name = "allowAction" + type = "ALLOW" + } + + actions { + name = "blockAction" + type = "RETURN_HTTP_RESPONSE" + code = 401 + + headers { + name = "Content-Type" + value = "application/json" + } + + body { + type = "STATIC_TEXT" + text = "{\"error\": \"Request blocked by WAF\"}" + } + } + + # --- Request Protection --- + request_protection { + + # XSS Protection + rules { + name = "xss-protection" + type = "PROTECTION" + action_name = "blockAction" + is_body_inspection_enabled = true + + protection_capabilities { + key = "941110" + version = 1 + } + + protection_capabilities { + key = "941100" + version = 1 + } + } + + # SQL Injection Protection + rules { + name = "sqli-protection" + type = "PROTECTION" + action_name = "blockAction" + is_body_inspection_enabled = true + + protection_capabilities { + key = "942100" + version = 1 + } + + protection_capabilities { + key = "942110" + version = 1 + } + } + + # Path Traversal Protection + rules { + name = "path-traversal-protection" + type = "PROTECTION" + action_name = "blockAction" + is_body_inspection_enabled = true + + protection_capabilities { + key = "930100" + version = 1 + } + + protection_capabilities { + key = "930110" + version = 1 + } + } + } + + # --- Rate Limiting --- + request_rate_limiting { + rules { + name = "rate-limit-300-per-min" + type = "RATE_LIMITING" + action_name = "blockAction" + + configurations { + period_in_seconds = 60 + requests_limit = 300 + action_duration_in_seconds = 60 + } + } + } +} + +# ----------------------------------------------------------------------------- +# WAF Firewall (attached to Load Balancer) +# ----------------------------------------------------------------------------- +resource "oci_waf_web_app_firewall" "main" { + compartment_id = var.compartment_ocid + display_name = "cisagent-waf" + backend_type = "LOAD_BALANCER" + load_balancer_id = oci_load_balancer_load_balancer.main.id + web_app_firewall_policy_id = oci_waf_web_app_firewall_policy.main.id + freeform_tags = var.freeform_tags +}