Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bffc1e1
fix: smoke test aligned after recent codebase changes
ritiksah141 May 29, 2026
95f52e9
Fixed the issue with the merge conflict in the dev branch. The confli…
ritiksah141 May 29, 2026
9027d11
Merge remote-tracking branch 'upstream/dev' into dev
ritiksah141 May 30, 2026
79d4b4b
Merge remote-tracking branch 'upstream/dev' into dev
ritiksah141 Jun 2, 2026
10acb52
Merge remote-tracking branch 'upstream/dev' into dev
ritiksah141 Jun 3, 2026
d9c917b
feat: wire live backend, implement missing endpoints, fix deploy crash
ritiksah141 Jun 3, 2026
504f022
fix: make all GET /api/* endpoints public for demo dashboard
ritiksah141 Jun 3, 2026
c098545
fix: update smoke tests for public GET auth model, accept scan timeout
ritiksah141 Jun 3, 2026
101201e
feat: image/video support, drag-drop upload, Vercel-ready config, ful…
ritiksah141 Jun 3, 2026
eb88b65
fix: apply CORS to all routes not just /api/* so /health is accessibl…
ritiksah141 Jun 3, 2026
d5ccb62
fix: scope score/findings/cve-summary to latest scan, make last-scann…
ritiksah141 Jun 3, 2026
a599a56
feat: implement resources, prioritization, drift, playbook endpoints;…
ritiksah141 Jun 4, 2026
b1feec3
fix: always prefer VITE_JWT_TOKEN over stale localStorage value
ritiksah141 Jun 4, 2026
aac1244
fix: compliance trend from scan history, fix AI tab crash
ritiksah141 Jun 4, 2026
6fea986
merge: resolve upstream/dev merge conflict in README.md
ritiksah141 Jun 4, 2026
0f7d915
merge: resolve upstream/dev README rule count conflict (36+3 PQC = 39…
ritiksah141 Jun 4, 2026
7dc3465
merge: resolve final README conflict in roadmap checklist
ritiksah141 Jun 4, 2026
e1696a0
feat: add PQC to discovery icon map and website rules gallery
ritiksah141 Jun 4, 2026
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,5 @@ __marimo__/
# Streamlit
.streamlit/secrets.toml
ai/vectorstore/
.vercel
.env*
71 changes: 44 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ Findings map to NIST FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), and FIPS 205 (SLH-DSA

| Feature | Description |
|---|---|
| **Misconfiguration Scanner** | Runs 30+ Azure security rules across storage, network, identity, database, compute, Key Vault, and post-quantum cryptography |
| **Misconfiguration Scanner** | Runs 39 Azure security rules across storage, network, identity, database, compute, Key Vault, and post-quantum cryptography |
| **Compliance Mapper** | Maps findings to CIS Benchmarks, NIST CSF, ISO 27001, and SOC 2 framework JSON files |
| **Scan History API** | Stores scans and findings in PostgreSQL and exposes findings, score, scan history, and compliance posture over REST |
| **Remediation Playbooks** | Every current rule ships with a matching Azure CLI remediation script |
| **Security Dashboard** | Frontend scaffold is present; the React dashboard MVP is still on the roadmap |
| **Scan History API** | Stores scans and findings in PostgreSQL and exposes findings, score, scan history, compliance posture, drift, and resource inventory over REST |
| **Remediation Playbooks** | Every rule ships with a matching Azure CLI remediation script (36 playbooks) |
| **Security Dashboard** | Full React dashboard deployed on Vercel — live monitoring, findings, compliance, drift, prioritization, and AI-layer views |
| **Project Website** | Documentation and reference site at [openshield-website.vercel.app](https://openshield-website.vercel.app) — blog, rules gallery, docs, roadmap, releases, and interactive playground |
| **Sentinel Integration** | Normalises findings and pushes them into Microsoft Sentinel via a Log Analytics custom table and KQL analytics rules |

---
Expand All @@ -57,13 +58,13 @@ Findings map to NIST FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), and FIPS 205 (SLH-DSA

```mermaid
flowchart TD
A["React Dashboard MVP\nPlanned frontend"]
A["React Dashboard\nVercel · Live"]
B["Flask REST API\nJWT · CORS · Blueprints"]
C["Scanner Engine\n30+ Python rules"]
C["Scanner Engine\n39 Python rules"]
D["Azure Subscription\nScanned via Azure SDK + Graph"]
E["Compliance Framework JSON\nCIS · NIST · ISO 27001 · SOC 2"]
F["PostgreSQL Database\nFindings · Scans"]
G["Azure CLI Playbooks\n30+ remediation scripts"]
G["Azure CLI Playbooks\n39 remediation scripts"]
H["sentinel/ingest.py\nNormalise + HMAC upload"]
I["Microsoft Sentinel\nOpenShieldFindings_CL · KQL rules"]

Expand All @@ -79,13 +80,15 @@ flowchart TD
I -->|alerts| A
```

## Live API
## Live Demo

The OpenShield API is deployed to the Render free tier and is accessible at:

**`https://openshield-api.onrender.com`**
| Service | URL |
|---|---|
| **Security Dashboard** (Vercel) | `https://openshield-gules.vercel.app` |
| **REST API** (Render) | `https://openshield-api.onrender.com` |
| **Project Website** | `https://openshield-website.vercel.app` |

> **Note:** As this is hosted on the Render free tier, the service may spin down after 15 minutes of inactivity. The first request after a spin-down can take 30-60 seconds to complete.
> **Note:** The API is hosted on Render. The dashboard connects automatically on load and shows live data from the PostgreSQL database.

> [!IMPORTANT]
> **Security Requirement:** Production deployments **fail at startup** if `JWT_SECRET` is missing, set to the insecure default, or shorter than 32 characters. Generate a strong secret with:
Expand All @@ -100,9 +103,10 @@ The OpenShield API is deployed to the Render free tier and is accessible at:

| Layer | Technology | Cost |
|---|---|---|
| Frontend | Scaffolded dashboard app (React + Tailwind planned) | Free |
| Project Website | Static HTML + Tailwind CDN, deployed on Vercel | Free |
| Security Dashboard | React + Vite + Tailwind, deployed on Vercel | Free |
| Backend API | Python + Flask | Free |
| Database | PostgreSQL | Free (Render/Azure free tier) |
| Database | PostgreSQL | Render managed PostgreSQL |
| Cloud Scanner | Python + Azure SDK | Free |
| Remediation | Azure CLI playbooks | Free |
| SIEM | Microsoft Sentinel | 90-day free trial |
Expand All @@ -128,7 +132,8 @@ openshield/
├── api/ # Flask REST API
│ ├── routes/
│ └── models/
├── frontend/ # Dashboard scaffold
├── frontend/ # React security dashboard (Vercel)
├── website/ # Project website — docs, blog, rules gallery (Vercel)
├── sentinel/ # Sentinel integration & KQL rules
├── .github/workflows/ # CI checks
├── docs/ # Documentation
Expand All @@ -141,6 +146,8 @@ openshield/

## Quick Start

**Backend (Flask API + Scanner)**

```bash
# Clone the repo
git clone https://github.com/openshield-org/openshield.git
Expand All @@ -154,6 +161,7 @@ export AZURE_SUBSCRIPTION_ID=your-subscription-id
export AZURE_CLIENT_ID=your-client-id
export AZURE_CLIENT_SECRET=your-client-secret
export AZURE_TENANT_ID=your-tenant-id
export JWT_SECRET=your-strong-secret # used to protect write endpoints (scan trigger, AI)

# Run a scan
python -c "
Expand All @@ -167,6 +175,21 @@ print(json.dumps(result, indent=2))
FLASK_APP=api/app.py flask run
```

**Frontend (React dashboard)**

```bash
cd frontend
npm install

# Local dev — points at http://localhost:5000 by default
npm run dev

# To develop against the live Render backend:
VITE_API_URL=https://openshield-api.onrender.com npm run dev
```

No token required — all read endpoints are public. Only scan trigger and AI endpoints require a JWT (POST only).

---

## Contributing
Expand All @@ -193,7 +216,7 @@ Contributors are credited below.
- [x] 30+ scan rules
- [x] Flask API + PostgreSQL schema
- [x] Post-quantum cryptography scanner (AZ-PQC-001 to AZ-PQC-003)
- [ ] React dashboard MVP
- [x] React dashboard (live on Vercel)
- [x] CIS Benchmark compliance mapping
- [x] SOC 2 compliance mapping
- [x] Sentinel alert integration
Expand All @@ -202,6 +225,8 @@ Contributors are credited below.
- [x] Azure CLI remediation playbook library
- [x] NIST CSF + ISO 27001 mappings
- [x] GitHub Actions CI pipeline
- [x] Project website with docs, blog, rules gallery, and playground
- [x] Live end-to-end data wiring (all API endpoints serving real data)
- [ ] Multi-cloud support (AWS, GCP)

---
Expand All @@ -212,20 +237,12 @@ MIT — free to use, modify, and distribute.

---

> Built with ❤️ by security engineers and students who believe cloud security tooling should be accessible to everyone.
> Built by security engineers and students who believe cloud security tooling should be accessible to everyone.

---

## Learn OpenShield

Explore the OpenShield learning portal to understand:

- Azure CSPM fundamentals
- OpenShield architecture
- Compliance mappings
- Remediation workflows
- Contributor onboarding
- Documentation navigation
Full documentation, the security rules gallery, blog, and interactive playground are available at the project website:

👉 [OpenShield Learn](docs/learn/index.html)
> Built by security engineers and students who believe cloud security tooling should be accessible to everyone.
**[openshield-website.vercel.app](https://openshield-website.vercel.app)**
20 changes: 15 additions & 5 deletions api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from flask_cors import CORS

from api.models.finding import DatabaseManager
from api.routes.ai import ai_bp

load_dotenv()

Expand All @@ -20,7 +19,12 @@
logger = logging.getLogger(__name__)

# Paths that do not require a JWT token
_PUBLIC_PATHS = {"/health", "/"}
# All GET requests are public — the dashboard is a public demo of seeded data.
# POST endpoints (scan trigger, AI) remain JWT-protected.
def _is_public_get(path: str) -> bool:
if path in ("/", "/health"):
return True
return path.startswith("/api/")

_INSECURE_JWT_DEFAULT = "change-me-in-production"
_MIN_JWT_SECRET_LENGTH = 32
Expand Down Expand Up @@ -108,7 +112,7 @@ def create_app() -> Flask:
"For production deployments, set this to your specific frontend domain(s)."
)
allowed_origins = allowed_origins_raw.split(",")
CORS(app, resources={r"/api/*": {"origins": allowed_origins}})
CORS(app, resources={r"/*": {"origins": allowed_origins}})

# ------------------------------------------------------------------ #
# Database Management #
Expand Down Expand Up @@ -140,7 +144,7 @@ def verify_jwt() -> None:
"""Validate the Bearer token on every non-public, non-OPTIONS request."""
if request.method == "OPTIONS":
return None
if request.path in _PUBLIC_PATHS:
if request.method == "GET" and _is_public_get(request.path):
return None

auth = request.headers.get("Authorization", "")
Expand All @@ -167,15 +171,21 @@ def verify_jwt() -> None:
# ------------------------------------------------------------------ #
from api.routes.ai import ai_bp
from api.routes.compliance import compliance_bp
from api.routes.drift import drift_bp
from api.routes.findings import findings_bp
from api.routes.prioritization import prioritization_bp
from api.routes.resources import resources_bp
from api.routes.scans import scans_bp
from api.routes.score import score_bp

app.register_blueprint(ai_bp)
app.register_blueprint(compliance_bp)
app.register_blueprint(drift_bp)
app.register_blueprint(findings_bp)
app.register_blueprint(prioritization_bp)
app.register_blueprint(resources_bp)
app.register_blueprint(scans_bp)
app.register_blueprint(score_bp)
app.register_blueprint(compliance_bp)

# ------------------------------------------------------------------ #
# Routes (public) #
Expand Down
32 changes: 25 additions & 7 deletions api/models/finding.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ def create_tables(self) -> None:
subscription_id TEXT NOT NULL,
started_at TIMESTAMPTZ NOT NULL,
completed_at TIMESTAMPTZ,
total_findings INTEGER DEFAULT 0
total_findings INTEGER DEFAULT 0,
score INTEGER DEFAULT NULL
);
""")
cur.execute("""
Expand Down Expand Up @@ -218,8 +219,8 @@ def save_scan(self, scan_result: Dict[str, Any]) -> None:
with conn.cursor() as cur:
cur.execute(
"""
INSERT INTO scans (scan_id, subscription_id, started_at, completed_at, total_findings)
VALUES (%s, %s, %s, %s, %s)
INSERT INTO scans (scan_id, subscription_id, started_at, completed_at, total_findings, score)
VALUES (%s, %s, %s, %s, %s, %s)
ON CONFLICT (scan_id) DO NOTHING
""",
(
Expand All @@ -228,6 +229,7 @@ def save_scan(self, scan_result: Dict[str, Any]) -> None:
scan_result["started_at"],
scan_result["completed_at"],
scan_result["total_findings"],
scan_result.get("score"),
),
)
for f in scan_result.get("findings", []):
Expand Down Expand Up @@ -290,6 +292,11 @@ def get_findings(self, filters: Optional[Dict[str, Any]] = None) -> List[Dict[st
if "scan_id" in filters:
clauses.append("scan_id = %s")
params.append(filters["scan_id"])
else:
# Default to the latest scan so historical findings do not inflate counts
clauses.append(
"scan_id = (SELECT scan_id FROM scans WHERE total_findings > 0 ORDER BY started_at DESC LIMIT 1)"
)

where = "WHERE " + " AND ".join(clauses) if clauses else ""
sql = f"SELECT * FROM findings {where} ORDER BY detected_at DESC LIMIT 1000"
Expand Down Expand Up @@ -350,15 +357,23 @@ def get_scans(self) -> List[Dict[str, Any]]:
# ------------------------------------------------------------------ #

def get_score(self) -> int:
"""Return a 0-100 security posture score based on open findings.
"""Return a 0-100 security posture score based on the latest scan's findings.

HIGH findings deduct 10 points each, MEDIUM 5, LOW 2.
Score floors at 0.
Scoped to the most recent scan so historical findings from older scans
do not accumulate and drive the score to zero.
HIGH findings deduct 10 points each, MEDIUM 5, LOW 2. Floors at 0.
"""
conn = self._get_conn()
with conn.cursor() as cur:
cur.execute(
"SELECT severity, COUNT(*) FROM findings GROUP BY severity"
"""
SELECT severity, COUNT(*)
FROM findings
WHERE scan_id = (
SELECT scan_id FROM scans WHERE total_findings > 0 ORDER BY started_at DESC LIMIT 1
)
GROUP BY severity
"""
)
rows = cur.fetchall()

Expand All @@ -379,6 +394,9 @@ def get_cve_summary(self) -> Dict[str, Any]:
AVG(cvss_score) as avg_cvss_score,
COUNT(CASE WHEN cvss_score >= 9.0 THEN 1 END) as critical_cve_count
FROM findings
WHERE scan_id = (
SELECT scan_id FROM scans WHERE total_findings > 0 ORDER BY started_at DESC LIMIT 1
)
""")
row = cur.fetchone()

Expand Down
Loading
Loading