Skip to content

feat(core): v0.4.0 - enterprise rules, OWASP migration, scan hardening, AI foundations, and signed releases - #348

Open
Vishnu2707 wants to merge 192 commits into
mainfrom
dev
Open

Vishnu2707 wants to merge 192 commits into
mainfrom
dev

Conversation

@Vishnu2707

Copy link
Copy Markdown
Collaborator

What does this PR do?

Promotes dev to main for the v0.4.0 release. Captures all work since v0.3.0 — enterprise rule packs, OWASP project migration, scan durability hardening, AI layer foundations, signed releases, supply chain security, frontend accessibility, and dependency security fixes.

Type of change

  • New scan rule
  • Bug fix
  • Dashboard/front-end work
  • API endpoint
  • Documentation
  • Compliance mapping

Key changes

Testing

  • All CI checks pass
  • No hardcoded credentials or secrets

Related issues

Closes #263, #303

Checklist

  • Every commit includes a DCO Signed-off-by trailer
  • I have not committed any real Azure credentials

Vishnu2707 and others added 30 commits April 25, 2026 15:07
* feat: add sentinel/ingest.py — Log Analytics ingestion via HMAC-SHA256

* feat: add sentinel/__init__.py

* feat: add KQL rule — HIGH severity finding detected

* feat: add KQL rule — misconfiguration wave detection

* feat: add KQL rule — new resource type critical detection

* Delete sentinel/rules directory

* Create rules

* Delete sentinel/rules

* Add KQL rule for high severity findings

* Add Misconfiguration Wave detection rule

* Add KQL rule for persistent misconfiguration detection

* Add KQL rule for new critical resource types

This rule identifies new resource types with critical findings that have occurred in the last 24 hours, excluding known types from the last 30 days.

* Add script to generate test findings in JSON format

This script generates test findings related to security compliance and saves them in a JSON file.

* Add Sentinel integration test plan and results

Added a comprehensive test plan for Sentinel integration, detailing test objectives, results, and acceptance criteria for various KQL rules and data ingestion.

* docs: add sentinel integration setup guide

Added a comprehensive setup guide for integrating Sentinel with Azure, covering prerequisites, workspace creation, activation, environment variable setup, ingestion, log verification, KQL rules deployment, and incident verification.
* Add az_net_003.py to check NSG rules for port 443

This script detects Network Security Groups (NSGs) with unrestricted inbound access on port 443 and provides remediation guidance.

* Add AZ-NET-004 rule for empty NSG detection

This script detects Network Security Groups (NSGs) that have no custom security rules configured, providing details for remediation.

* Add AZ-NET-005 rule for DDoS protection check

This script detects virtual networks in Azure that do not have DDoS protection enabled and provides remediation steps.

* feat: add rule AZ-NET-006 — public IP unassociated with any resource

This rule detects public IP addresses that are not associated with any resource, providing details for remediation.

* feat: add rule AZ-NET-007 — Application Gateway without WAF enabled

This rule detects Application Gateways that do not have WAF enabled, logging findings and providing remediation steps.

* feat: add rule AZ-NET-008 — load balancer with no backend pool

This rule detects load balancers in Azure that are not configured with a backend pool, indicating potential misconfiguration or unnecessary costs.

* feat: add rule AZ-NET-009 — VPN gateway using outdated IKE version

This script detects VPN gateways using the outdated IKEv1 protocol and provides remediation steps to migrate to IKEv2.

* feat: add rule AZ-NET-010 — subnet with no NSG attached

This script detects subnets in Azure that do not have a Network Security Group (NSG) attached, logging findings and providing remediation guidance.

* feat: add playbook fix_az_net_003.sh

This script updates the NSG rule to restrict inbound traffic on port 443 to a specified IP range.

* feat: add playbook fix_az_net_004.sh

This script adds a default deny-all inbound rule to a specified NSG.

* feat: add playbook fix_az_net_005.sh

This script enables DDoS protection on a specified virtual network in Azure. It checks for required parameters and provides usage instructions if they are missing.

* feat: add playbook fix_az_net_006.sh

This script deletes unassociated public IP addresses in Azure.

* feat: add playbook fix_az_net_007.sh

This script enables WAF on an Application Gateway, ensuring compliance with the AZ-NET-007 rule.

* feat: add playbook fix_az_net_008.sh

Script to remediate AZ-NET-008 by deleting empty load balancers.

* feat:add script to update VPN connection to IKEv2

This script updates a VPN connection to use IKEv2, ensuring compliance with the AZ-NET-009 rule.

* feat: add playbook fix_az_net_010.sh

This script attaches a specified network security group to a given subnet in a virtual network, ensuring compliance with the AZ-NET-010 rule.

* Clarify description and add note for public-facing services

Updated the description to clarify the risk of exposing port 443 and added a note regarding public-facing services.

* Change severity level from MEDIUM to HIGH

* fix: AZ-NET-005 severity changed to LOW — DDoS Standard high cost on small subscriptions

* Add note about NetworkManagementClient usage

Added a note regarding the creation of NetworkManagementClient directly and suggested a follow-up for consistency.

* Add note about NetworkManagementClient usage

Added a note regarding the use of NetworkManagementClient and suggested a follow-up for consistency.

* Add additional security controls to CIS Azure benchmark

* Refine control descriptions in nist_csf.json

Updated descriptions for various controls to enhance clarity and specificity regarding remote access management, data protection, and security measures.

* fix: add AZ-NET-003 to AZ-NET-010 to ISO27001 compliance framework

Updated descriptions for various controls to clarify compliance requirements and improve security guidance.

---------

Co-authored-by: Vishnu Ajith <86302373+Vishnu2707@users.noreply.github.com>
* feat: add rule AZ-STOR-003 storage lifecycle policy check

* feat: add rule AZ-STOR-003 storage lifecycle policy check
* docs: add SOC 2 Type II compliance framework mapping for all 20 rules

Added SOC 2 Type II framework with detailed controls for security measures and compliance requirements.

* feat: add soc2 to FRAMEWORK_FILE_MAP in finding.py

add soc2.json to FRAMEWORK_FILE_MAP in finding.py

* feat: add soc2 to SUPPORTED_FRAMEWORKS in compliance.py

Added 'soc2' to the list of supported compliance frameworks.

* Add SOC 2 controls for data protection and management
* refactor: add get_virtual_networks() and get_public_ip_addresses() to AzureClient

* Refactor DDoS protection check to use azure_client

* refactor: AZ-NET-006 now uses azure_client.get_public_ip_addresses()
- Python syntax check on all rule files
  - Rule structure validation (RULE_ID, SEVERITY, FRAMEWORKS) + RULE_ID uniqueness
  - Hardcoded credential scan
  - Playbook existence + bash syntax check for every rule
  - Compliance JSON validation for all four framework files (inc. soc2.json)
  - API syntax check
  - Compliance vs rule cross-reference check
  - CI summary step with per-check pass/fail table (if: always)
  - Fix duplicate DESCRIPTION assignment in az_net_003.py
  - Add pyyaml to requirements.txt for local YAML validation
  - Add docs/ci-pipeline.md with local run commands and design rationale
  - Update CI_PIPELINE_GUIDE.md with final PR description

Closes #30
parthrohit22 and others added 23 commits August 29, 2026 01:24
* docs: update learn page statistics and add CI/CD workflow

Signed-off-by: Parth Rohit <parthrohit60@gmail.com>

* fix: push to branch explicitly, regenerate stats from current dev

update-learn-page.yml: actions/checkout leaves the runner in detached
HEAD, so the final `git push` had no upstream and would fail with
"You are not currently on a branch". Push HEAD to the triggering ref
by name instead.

Rebased onto dev and re-ran update_learn_page.py against the current
rule set rather than hand-editing numbers that would be stale again
immediately. The README feature-table regex still had the old
rule-category wording from before this branch was cut, so it failed
loud as designed instead of silently no-op'ing; updated it to match
the current sentence.

Removed the "known cleanup item" note about KeyVault vs Key Vault
naming now that the category chart renders the declared category
string directly, and corrected the Known Gaps bullet that still said
README.md counts were updated by hand, now that render_readme() covers
both files.

Signed-off-by: Parth Rohit <parthrohit60@gmail.com>

* fix: avoid inline workflow-context interpolation in push step

github-advanced-security flagged interpolating ${{ github.ref_name }}
directly into the Commit and push run: script as a potential
script-injection pattern. Pass it through an env var instead and
reference it as a shell variable, which keeps the same behavior
(push HEAD to the triggering ref) without templating untrusted
workflow context straight into a shell command.

Signed-off-by: Parth Rohit <parthrohit60@gmail.com>

* fix(docs): warn about excluded severity counts

Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>

* style(docs): format learn page updater

Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>

* fix(docs): count playbooks by rule match, not raw file glob

count_playbooks() globbed every *.sh in playbooks/cli/, which also picked
up review_enterprise_resilience.sh -- a shared helper several fix_*.sh
wrappers exec into, not a playbook any single rule owns. That inflated
the count past rule_count (95 rules / 96 playbooks) and broke the "every
rule ships with a matching playbook" claim in the README.

find_matching_playbooks() now mirrors the fix_<rule filename stem>.sh
convention ci.yml's playbook_check step already enforces, derives the
count from actual rule-to-playbook matches, and fails loudly if a rule
has no matching playbook or the two counts disagree. Unmatched files
(shared helpers) are reported as a non-fatal warning instead of silently
inflating the total.

Also: collect_rule_stats() now skips files with no parseable RULE_ID,
matching count_rules()'s own filter, so the severity-box totals can no
longer disagree with the headline rule count; and render_category_rows()
HTML-escapes the category name before embedding it.

Regenerated docs/learn/index.html and README.md: 95 rules / 95 playbooks.

Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>

---------

Signed-off-by: Parth Rohit <parthrohit60@gmail.com>
Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>
* Add enterprise network perimeter controls

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* Fix DNS evidence and WAF logging checks

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* Fix firewall threat intel and WAF rate checks

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* Preserve unknown perimeter evidence states

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

---------

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>
…320)

Partially addresses #294 - the immediate, in-repo containment slice.
Not a fix for the full issue: durable multi-tenant authorization
(Entra/OIDC Authorization Code + PKCE, JWKS validation, per-tenant
schema/RLS) is a separate, larger body of work that needs an actual
identity-provider app registration against this deployment's real
domains, which is outside what a PR to this repository can provision
or decide alone. That part stays open and un-claimed here.

What this closes, confirmed live before this commit (verify_jwt() in
api/app.py only checked the signature and expiry-if-present; g.user
was never read by any route; the baseline schema has no tenant/org
column at all):

- A JWT with no exp claim decoded successfully forever - the
  documented VITE_JWT_TOKEN demo credential had no expiry so it
  works without rotation, per its own prior docstring. jwt.decode()
  now requires exp; a token missing it is rejected the same as an
  invalid signature.
- role was decoded into g.user but never checked by any route, so a
  viewer-role token could call every write endpoint (scan trigger,
  every AI endpoint) exactly like an admin token. verify_jwt() now
  requires a recognized role (viewer/operator/admin) on every
  accepted token, and rejects any non-GET/HEAD request from a viewer
  token with 403 - enforced once, in the middleware, so it covers
  every current and future write route uniformly rather than needing
  a per-route check. Confirmed this still holds under
  OPENSHIELD_PUBLIC_DEMO=true - demo mode only ever widens read
  access, never write.
- POST /api/scans/trigger accepted an arbitrary caller-supplied
  subscription_id with no authorization check - any operator/admin
  token could trigger a scan against any subscription. Added an
  optional OPENSHIELD_AUTHORIZED_SUBSCRIPTIONS allowlist (issue
  #294's own suggested interim for a single-tenant deployment); left
  unset, behavior is unchanged from today, but the API now logs a
  loud startup warning when it is, matching the existing
  ALLOWED_ORIGINS/JWT_SECRET convention in the same function.
- scripts/generate_demo_jwt.py minted a permanent, no-expiry token by
  design. It now requires an expiry (DEMO_JWT_TTL_HOURS, default
  24h) and its docstring/output no longer claim the token works
  without rotation.
- docs/api-reference.md and .env.example document the new
  requirements and the allowlist variable.

Deliberately left untouched: frontend/src/App.jsx's local-dev
dev-local-token bootstrap. It is not a valid signed JWT (verify_jwt()
would reject it outright outside demo mode) and only ever worked
because OPENSHIELD_PUBLIC_DEMO bypasses the JWT check for GET -
removing it without a replacement login flow would break local dev
for every contributor while fixing nothing real, since it was never
a working credential to begin with. The actual leaked-credential risk
is VITE_JWT_TOKEN, which is what this commit bounds with expiry and
role.

Verified: full backend suite (796 passed, 5 skipped - pre-existing/
environment-only, unrelated - confirmed identical skip count against
unmodified dev), including all pre-existing tests/test_auth.py and
tests/test_async_scan_persistence.py cases (every existing token
fixture in this repo already carried role+exp, so nothing broke).
New coverage in tests/test_auth.py (missing-exp, missing-role,
unrecognized-role, viewer-read-allowed, viewer-write-rejected
including under demo mode, operator/admin-write-allowed) and the new
tests/test_subscription_authorization.py (unset allowlist preserves
today's behavior, allowlisted/non-allowlisted subscriptions,
case-insensitive comparison, blank-entry handling, startup-warning
presence/absence). ruff check and format --check clean.

Signed-off-by: Parth J Rohit <parthrohit60@gmail.com>
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
…les (AZ-IDN-016-025) (#279)

* feat: complete enterprise data protection rules

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* style: format storage rule tests

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix: satisfy rule validation and refresh image packages

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* feat: implement enterprise privileged access and identity security rules (AZ-IDN-016-025)

Add 10 new rules covering privileged identity security for Microsoft Entra ID:
- AZ-IDN-016: Privileged user missing phishing-resistant MFA (CRITICAL)
- AZ-IDN-017: Global Administrator permanently assigned outside PIM (HIGH)
- AZ-IDN-018: Privileged role assigned outside PIM (HIGH)
- AZ-IDN-019: Stale privileged account retains active access (HIGH)
- AZ-IDN-020: No emergency access accounts detected (HIGH)
- AZ-IDN-021: Legacy authentication protocols not blocked (HIGH)
- AZ-IDN-022: No MFA requirement for Azure Management (HIGH)
- AZ-IDN-023: Identity Protection risk policies disabled (MEDIUM)
- AZ-IDN-024: Service principals excluded from MFA enforcement (MEDIUM)
- AZ-IDN-025: Privileged role-assignable group has no owner (MEDIUM)

Add 5 new Graph API collectors to azure_client.py:
- get_privileged_role_members, get_privileged_users_mfa_methods,
  get_pim_role_assignments, get_identity_protection_policies,
  get_privileged_groups

Add MockAzureClient support for all new collectors (fully offline tests).
Add 58 tests in test_rules_identity_priv.py covering compliant, violating,
empty inventory, API failure (None), and edge cases.
Add 10 remediation playbooks (fix_az_idn_016.sh through fix_az_idn_025.sh).
Add compliance mappings for CIS Azure 2.0.0, NIST CSF, ISO 27001, SOC 2.
Add docs/rules-reference.md entries for all 10 rules.

Closes #258

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(scanner): correct privileged identity evaluation

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(scanner): address review blockers in privileged access and identity rules

PIM detection (IDN-017, IDN-018):
- Collector now queries both roleAssignmentSchedules (Active) and
  roleEligibilitySchedules (Eligible) so eligible assignments are
  actually detected; previously only Active schedules were fetched
- principalId/principalType tracked for all principal types (users,
  groups, service principals), not only users

Identity Protection (IDN-023):
- Replace unreliable direct policy endpoints with CA-policy inspection:
  checks conditions.userRiskLevels / signInRiskLevels on enabled policies

CA policy user scope (IDN-021, IDN-022):
- _covers_all_users() helper added; policies that target a subset of
  users no longer suppress a tenant-wide finding

Workload identity exclusion (IDN-024):
- Fix excludeServicePrincipals check: the field holds SP IDs, not All;
  rewrite to use includeServicePrincipals presence and All-exclude logic

Collection failures (Cosmos DB, Redis cache):
- get_cosmos_accounts / get_managed_caches return None on failure instead
  of []; callers log a warning and skip rather than treating missing
  inventory as compliant

Enum normalization (AZ-STOR-009, AZ-DB-007):
- Replace raw str() calls with enum_str() for SDK enum fields
- Fix immutability retention property:
  period_since_creation_in_days -> immutability_period_since_creation_in_days

Test fixtures updated to match new API contracts (principalId, deep-merge
of CA policy conditions, correct immutability property name).

CI: add chromadb CVE-2026-45830 and CVE-2026-45833 to pip-audit ignore
list (no patched version; removal tracked in PR #317).

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(tests): update vector-store skip guard to check bm25_index.json

The skip condition checked for the ai/vectorstore/ directory, which can
exist from a previous build without the BM25 index file. After PR #317
replaced chromadb with a JSON BM25 index, the test ran and raised
VectorStoreNotBuilt instead of skipping. Changed skipif to check for
the actual index file so the class is skipped correctly when the index
has not been built.

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(lint): shorten E501 line in test_ai_hallucination_guard.py

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

---------

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
)

* fix(api): rate-limit /ready and /metrics, expose db pool telemetry

Closes the remaining scope of #296. The connection-checkout leak itself
was already fixed by #306 (g.db + the teardown handler, with a real
PostgreSQL-backed integration test proving connections balance across
repeated readiness probes). What was still open: /ready and /metrics
are unauthenticated by design (probe/scrape endpoints must never
require a token), which also made them the one place an
unauthenticated caller could trigger repeated pooled-connection work
with no rate limiting at all, and there was no visibility into how
close the pool was to exhaustion before it happened.

- Add api.observability.probe_rate_limit: an in-memory, per-process,
  per-source-IP rate limiter for probe/scrape endpoints. Deliberately
  not the existing Postgres-backed api.rate_limit.rate_limit, which
  would add a database round trip (and a second, separately-tracked
  pooled connection under its own g.db) to the exact endpoint whose
  job is to protect the database from overload. The connection pool
  it guards is itself process-local under Gunicorn's multi-worker
  model, so a per-process budget is the matching granularity, not a
  weaker substitute for a shared one. Wired onto /ready (budget of 5
  per 10s per source IP, half the default DB_POOL_MAX_CONN) and
  /metrics (20 per 10s, generous for normal Prometheus scrape
  intervals). The check runs before the view body, so a rejected
  request never reaches the database work it would otherwise trigger.
- Add api.models.finding.get_pool_stats(): a point-in-time snapshot of
  the shared pool's in-use/idle/max-connection counts and utilization
  percentage. Reports only counts - never the DSN, host, or
  credentials - so it's safe on a public surface. Returns zeroed stats
  before any connection has been made instead of raising.
- Wire get_pool_stats() into three new Prometheus gauges
  (openshield_db_pool_connections_in_use/idle/max), refreshed lazily
  on every /metrics scrape via a provider callback registered from
  api/app.py - api/observability.py stays free of project imports
  (its own documented constraint, since the worker reuses it too) by
  never importing api.models.finding directly.
- Document in docs/deployment/render.md that Render's Blueprint format
  has no path-based access control, so the in-app rate limiter is a
  defense-in-depth backstop, not a substitute for restricting network
  reachability to /ready and /metrics at whatever reverse proxy/CDN/
  WAF fronts a real deployment - that configuration is operational,
  outside what render.yaml can express.

New tests in tests/test_readiness_hardening.py cover: the rate
limiter's budget/window/per-IP-isolation/testing-bypass/key-pruning
behavior in isolation, /ready actually rejecting a source once its
budget is spent without touching the database for the rejected
request, get_pool_stats()'s zero/nonzero/never-leaks-the-dsn
behavior, and /metrics rendering the three new gauges (and surviving
a broken stats provider without failing the whole scrape).

Verified: full backend suite (796 passed, 3 skipped - pre-existing,
unrelated), including tests/test_observability.py's real
PostgreSQL-backed readiness-leak test run against a local Postgres
instance to confirm the new decorator doesn't disturb #306's fix;
ruff check and format --check clean.

Signed-off-by: Parth J Rohit <parthrohit60@gmail.com>
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>

* fix(api): restore dropped constant, bound probe-limiter memory

Two fixes on top of the dev merge that landed on this branch:

1. The merge into dev (which now carries #294/#320's own changes to
   api/app.py) dropped this branch's _READY_MAX_REQUESTS_PER_WINDOW
   constant definition while keeping its usage on the /ready route,
   leaving api/app.py with an undefined name that only surfaced at
   create_app() call time (ruff's F821 caught it as CI's first
   failure; Backend Tests failed for the same underlying reason).
   Restored the constant and its comment.

2. m-khan-97's review: probe_rate_limit's per-key cleanup only ever
   prunes the exact (address, path) key the current request touches.
   A caller that continuously rotates its source address - or a
   spoofed forwarded address wherever the trusted-proxy boundary is
   misconfigured - creates a new one-shot dictionary entry per
   address that's never revisited and therefore never pruned,
   making the limiter's own tracking dict an unbounded memory sink.

   Fixed with the two things asked for:
   - A periodic global sweep (every _PROBE_SWEEP_INTERVAL calls, not
     every call - a full-dict scan per request would defeat the
     point of a cheap in-memory limiter) that prunes every key whose
     hits have all expired, not just the current request's key.
   - A hard cap (_PROBE_MAX_TRACKED_KEYS) on distinct tracked keys,
     with deterministic least-recently-touched eviction via an
     OrderedDict instead of the previous plain dict - every hit
     (including one that just survives a sweep) moves its key to the
     end, so eviction always drops the coldest entry first.
   Also added the suggested (non-blocking) Retry-After header on 429.

New tests in tests/test_readiness_hardening.py cover exactly the
scenario m-khan-97 described: many distinct one-shot addresses,
advance past the window, trigger cleanup through different addresses,
and prove the stale keys are gone and the map stays bounded. Plus
direct hard-cap/LRU-eviction tests and the Retry-After header.

Verified: full backend suite (862 passed, 5 skipped - pre-existing/
environment-only), including all 17 tests in
tests/test_readiness_hardening.py and the pre-existing
tests/test_auth.py / tests/test_observability.py suites unaffected.
ruff check and format --check clean.

Signed-off-by: Parth J Rohit <parthrohit60@gmail.com>
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>

* fix(api): address TFT444's 3 probe-limiter findings

1. move_to_end(key) ran on every request including rejected ones, so
   an attacker hammering an already-exhausted key kept it permanently
   at the back of the eviction order while quiet, legitimate keys
   drifted toward the front and got evicted instead - defeating the
   memory cap's actual purpose for exactly the caller it exists to
   bound. Now only a request that counts against the budget (allowed)
   refreshes a key's position; a rejected request leaves it wherever
   it already was.

2. _probe_hits is one dict shared by every probe_rate_limit-decorated
   endpoint, but the periodic sweep applied whichever endpoint's
   request happened to trigger it - its own window_seconds - to every
   tracked key regardless of which endpoint's window actually applies
   to it. Dormant today since /ready and /metrics both default to the
   same 10s window, but the first endpoint added with a different one
   would have caused premature resets or lingering stale entries
   across every other endpoint's keys. Added _ProbeEntry to carry
   window_seconds alongside each key's own hits deque, so the sweep
   (and the per-call prune) always uses the window that key was
   actually registered under.

3. Documented the ProxyFix(x_for=1) trust boundary explicitly in
   api/app.py: what it assumes (Render's edge is the only thing able
   to append to X-Forwarded-For before this process sees it) and what
   breaks if that's violated (a directly-reachable origin lets a
   caller set their own forwarded IP per request, which is equivalent
   to no per-IP rate limiting at all for every control that depends on
   request.remote_addr).

New regression tests for both behavioral fixes: one proves a caller
hammering an exhausted key doesn't stay artificially warm while a
quiet legitimate key gets evicted in its place; the other runs two
endpoints with different window_seconds sharing the tracked-key dict
and proves a sweep triggered by one doesn't misapply its window to
the other's entries.

Verified: full backend suite (864 passed, 5 skipped - pre-existing/
environment-only), all 19 tests in tests/test_readiness_hardening.py
including the 2 new ones, ruff check and format --check clean.

Signed-off-by: Parth J Rohit <parthrohit60@gmail.com>
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>

---------

Signed-off-by: Parth J Rohit <parthrohit60@gmail.com>
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [postcss-selector-parser](https://github.com/postcss/postcss-selector-parser).


Updates `postcss-selector-parser` from 6.1.2 to 6.1.4
- [Release notes](https://github.com/postcss/postcss-selector-parser/releases)
- [Changelog](https://github.com/postcss/postcss-selector-parser/blob/main/CHANGELOG.md)
- [Commits](postcss/postcss-selector-parser@v6.1.2...6.1.4)

---
updated-dependencies:
- dependency-name: postcss-selector-parser
  dependency-version: 6.1.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the npm_and_yarn group with 1 update in the /frontend directory: [browserslist](https://github.com/browserslist/browserslist).


Updates `browserslist` from 4.28.2 to 4.28.9
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](browserslist/browserslist@4.28.2...4.28.9)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* feat(frontend): add API request timeout support

Signed-off-by: Shaurya K Sharma <shauryaksharma24@gmail.com>

* fix(frontend): preserve transient API fallback semantics

Signed-off-by: Shaurya K Sharma <shauryaksharma24@gmail.com>

---------

Signed-off-by: Shaurya K Sharma <shauryaksharma24@gmail.com>
* website: migrate to Astro + Decap CMS with GitHub Pages pipeline

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* website: commit src/lib, bump astro to patched 7.x, drop obsolete CI job

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* website: build hero legend and rule info with DOM APIs, no innerHTML

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* fix(website): resolve manual test findings across hero, docs, feed and articles

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* feat(website): complete site and automate Pages deployment

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* fix(website): keep CMS optional and resolve CodeQL

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* ci: include website validation in required summary

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* style(ci): keep website gate comment concise

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* fix(ci): preserve container workflow contract

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

---------

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>
* feat: complete enterprise data protection rules

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix: satisfy rule validation and refresh image packages

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(scanner): address review blockers in enterprise data-protection rules

- Move AZ-STOR-009 opt-in check from BlobContainer (no ARM tags) to
  the parent storage account, which exposes tags via the SDK; all
  containers under a tagged account are now evaluated for immutability.
- Replace incorrect NIST mapping A.12.4.1 (ISO 27001) on AZ-DB-007
  with PR.PT-1 across az_db_007.py, nist_csf.json, and rules-reference.
- Add executable az CLI commands to fix_az_cache_001, fix_az_cosmos_001,
  fix_az_cosmos_002, fix_az_db_005, fix_az_db_006, and fix_az_db_007
  playbooks; each validates the target and requires APPLY confirmation
  before modifying any Azure resource.
- Update storage-protection-controls.md to document the account-level
  tagging scope for AZ-STOR-009.

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(scanner): check immutability tag on container, not account (AZ-STOR-009)

The policy_required guard was placed at the account level, but the
oshield:immutability-required tag is set per container. Moving the
check inside the container loop allows containers with the tag to be
evaluated regardless of whether the parent account carries it.

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(scanner): check immutability tag on container or parent account (AZ-STOR-009)

The policy_required guard was placed at the account level only, but the
oshield:immutability-required tag may be set per-container or per-account.

Now uses OR logic: a container is evaluated if the account carries the
requirement tag (protecting all containers) OR if the container itself
carries it (per-container opt-in). Both cases were previously broken:
the account-level check did not reach container-tagged resources, and
no per-container check existed at all.

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(scanner): address storage rule correctness gaps in AZ-STOR-006/007/008

- AZ-STOR-006: treat allow_shared_key_access=None as insecure (Azure
  documents unset as equivalent to True); only False is compliant
- AZ-STOR-007: treat minimum_tls_version=None as TLS 1.0 (Azure default);
  use enum_str() instead of str() to handle SDK enum objects correctly
- AZ-STOR-008 playbook: fix Key Vault URI parsing; the previous bash
  expansion passed the wrong segments to --encryption-key-vault and
  --encryption-key-name; now splits vault URI, key name, and optional
  key version correctly
- ci.yml: remove CVE-2026-45830 and CVE-2026-45833 pip-audit exclusions
  (chromadb CVEs unrelated to this PR; resolved by PR #317)

Adds regression tests for None-as-default behavior and SDK enum handling
in AZ-STOR-006 and AZ-STOR-007 (22 storage tests, all passing).

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(scanner): address all review feedback and CI failures for PR #278

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

---------

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
…336)

* build: lock Python runtime and development dependencies with hashes

Signed-off-by: Muhammad Ibrahim <135441675+m-khan-97@users.noreply.github.com>

* fix(ci): isolate lock tooling outside the source checkout

Signed-off-by: Muhammad Ibrahim <135441675+m-khan-97@users.noreply.github.com>

---------

Signed-off-by: Muhammad Ibrahim <135441675+m-khan-97@users.noreply.github.com>
Signed-off-by: ritiksah141 <ritiksah141@gmail.com>
…attestations (#339)

Signed-off-by: Muhammad Ibrahim <135441675+m-khan-97@users.noreply.github.com>
Signed-off-by: ritiksah141 <ritiksah141@gmail.com>
…ion (#294) (#345)

* fix(auth): remove dashboard bearer token and add OIDC token verification (#294)

Frontend
- remove the VITE_JWT_TOKEN bootstrap and dev-local-token fallback
- keep bearer tokens in memory only and purge legacy localStorage tokens
- fail CI if a JWT-shaped value reaches the public bundle

API
- move token verification into api/auth.py with two modes:
  shared_secret (HS256, now also requires sub, optional iss/aud) and
  oidc (JWKS-verified asymmetric tokens with issuer, audience, expiry,
  issued-at, subject, tenant allowlist and IdP app-role mapping)
- refuse HS256/none in oidc mode, fail closed with 503 when JWKS is
  unreachable, and refuse to start on incomplete oidc configuration
- warn when shared_secret mode runs in production

Docs
- authentication setup, containment checklist and JWT_SECRET rotation
- demo JWT script is now short-lived and for API testing only

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>

* fix(auth): resolve SAST and credential-scan findings

Rename the shared-secret mode constant so the credential scan does not
read it as a hardcoded secret, generate the test signing secret at run
time, and reword rejection log messages flagged by Semgrep.

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>

---------

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
)

* fix(scanner): rebase compute-rule corrections onto current dev

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>

* test(scanner): drop duplicated AZ-CMP-007 test block from bad rebase

A stale copy of the AZ-CMP-007 helpers and tests survived the dev rebase,
sitting ahead of the AZ-CMP-004 assessment tests. Its local `def _subnet_id(name)`
shadowed the module-level `_subnet_id(vnet_name, subnet_name)`, breaking seven
AZ-CMP-001 tests with "takes 1 positional argument but 2 were given", and it
carried two stray `f[...]` asserts that tripped ruff F811/F821 (11 errors).

The canonical AZ-CMP-007 block at end of file (renamed helper `_jit_subnet_id`,
no stray asserts) is kept. ruff clean; test_rules_compute.py 56 passed.

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>

* style(tests): add missing blank line between mock_azure methods

`ruff format --check` (run alongside `ruff check` in the Lint job) flagged
tests/helpers/mock_azure.py — set_vm_patch_status, added earlier in this
PR, had no blank line before it. The earlier `ruff check` F811 failure had
masked this.

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>

---------

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
…273)

* feat: add rule AZ-CMP-005 Trusted Launch (Secure Boot + vTPM) check

Adds a Compute scan rule that flags Generation 2 VMs which do not have
Trusted Launch fully enabled (security type TrustedLaunch with both Secure
Boot and vTPM on). Without them, unsigned or malicious code can run during
boot and persist beneath the OS, evading OS-level antimalware and EDR.

Generation 1 VMs cannot use Trusted Launch and are treated as NOT_APPLICABLE:
a VM's list_all() representation does not carry its Hyper-V generation, so the
rule resolves the OS disk's hyper_v_generation to confirm a VM is Gen2 before
flagging, and never raises a false finding against Gen1 hardware. A security
type already declared as TrustedLaunch is itself Gen2-only, so a
Secure-Boot/vTPM-off VM there is flagged without a disk lookup. Confidential
VMs (which provide Secure Boot and vTPM by construction) are out of scope, and
a VM whose generation cannot be confirmed is left unflagged rather than
risking a Gen1 false positive.

Includes the remediation playbook (playbooks/cli/fix_az_cmp_005.sh) and maps
the rule across the four compliance frameworks: NIST CSF PR.DS-6, ISO 27001
A.12.5.1, and SOC 2 CC6.8. CIS uses the repository's existing N/A convention
(as in AZ-KV-001) because CIS Azure Foundations 2.0.0 has no dedicated Trusted
Launch recommendation. Adds six unit tests covering the compliant,
non-compliant Gen2, TrustedLaunch-declared-but-vTPM-off, Gen1 NOT_APPLICABLE,
unknown-generation, and Confidential-VM cases.

Closes #269

Signed-off-by: shariqueahmad108-ship-it <shariqueahmad108@gmail.com>

* fix(playbook): guard fix_az_cmp_005.sh args under set -u

Use ${1:-}/${2:-} so running the playbook with missing arguments prints the
usage message and exits 1, instead of crashing on an unbound variable under
`set -euo pipefail`. Matches the convention in fix_az_net_016.sh.

Addresses review feedback from @Vishnu2707 on #273.

Signed-off-by: shariqueahmad108-ship-it <shariqueahmad108@gmail.com>

---------

Signed-off-by: shariqueahmad108-ship-it <shariqueahmad108@gmail.com>
* fix(scanner): resolve COR-001-004 scanner correctness issues (#151)

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* fix(scanner): normalise Azure SDK enum fields in AZ-NET-003 and AZ-DB-002

Addresses review feedback on PR #163:

- Add a shared enum_str() helper in azure_client.py that safely unwraps
  Azure SDK enum fields via .value, since str(enum_member) yields e.g.
  'SecurityRuleDirection.INBOUND' rather than 'Inbound' and silently
  breaks naive string comparisons against real SDK objects.
- AZ-NET-003: normalise direction, access, and source_address_prefix
  through enum_str() so real SecurityRuleDirection/SecurityRuleAccess
  enum values are detected correctly, not just plain-string mocks.
- AZ-DB-002: normalise the auditing policy state through enum_str() so
  a real BlobAuditingPolicyState.ENABLED value is not mistaken for
  disabled (false positive) or vice versa.
- AZ-DB-002: malformed ARM IDs are now logged explicitly instead of
  silently skipped.
- AZ-NET-003: the matched plural source_address_prefixes entry is now
  included in finding metadata.
- Add regression tests using real azure-mgmt-network / azure-mgmt-sql
  SDK model classes (SecurityRule, SecurityRuleDirection,
  SecurityRuleAccess, ServerBlobAuditingPolicy, BlobAuditingPolicyState)
  rather than only SimpleNamespace/string-backed mocks, per
  SHAURYAKSHARMA24's review.
- Sync branch with upstream dev (v0.3.0) and apply current ruff format
  gate, per ritiksah141's review.

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* fix: remove duplicate _diagnostic_settings init line

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* test: add identity rule regression coverage

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* fix: improve AZ-NET-003 scanner correctness

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* fix: resolve ruff lint errors

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* test: restore az_net_016/017 imports dropped in merge

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* chore: trigger CI

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* fix: add compliant plural-port test, drop unrelated identity test reformatting

Addresses parthrohit22's review: adds a compliant-case regression test for the destination_port_ranges fix in AZ-NET-003, and resets tests/test_rules_identity.py to dev's formatting, keeping only the one genuine new test (test_idn_007_disabled_user_without_mfa_returns_no_findings).

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* style: apply ruff format to az_net_003.py

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

* fix(scanner): AZ-DB-002 emits indeterminate LOW finding on failed policy lookup

Addresses TFT444's review: previously a failed auditing-policy lookup (API/auth failure) was silently skipped. This aligns with the AZ-CMP-002 convention by emitting a LOW-severity finding with metadata.determination = 'indeterminate' instead, so the verification gap stays visible in scan output rather than disappearing silently.

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>

---------

Signed-off-by: safidnadaf <safidnadaf25@gmail.com>
Co-authored-by: safidnadaf <safidnadaf@users.noreply.github.com>
* feat: add VM Scale Set inventory collector and rule AZ-CMP-005

Adds AzureClient.get_virtual_machine_scale_sets() (list_all across the
subscription, following the get_virtual_machines() pattern) and its
MockAzureClient test double.

Ships AZ-CMP-005 as the first rule to use it: flags VMSS network
interface configurations that provision a public IP with no NSG
attached, the VMSS-template equivalent of AZ-CMP-001's per-VM NIC
check. Detection reads the network interface configuration template
directly (network_interface_configurations[].ip_configurations[] /
.network_security_group) rather than resolving separate NIC resources,
since a VMSS profile embeds these settings inline.

Includes a remediation playbook (az vmss update --set on the network
profile, with an explicit warning about the required instance
upgrade), collector and rule tests, and compliance framework mappings.
CIS is mapped to N/A-CMP-005 following the repository's established
convention for the same real control (7.1, owned by AZ-CMP-001)
applied to a second resource type, since the one-CIS-ID-per-rule
convention doesn't allow reusing 7.1 directly.

Closes #271

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>

* fix: renumber AZ-CMP-005 to AZ-CMP-006, fix subnet-NSG false positive

AZ-CMP-005 collided with #273 (Trusted Launch check), opened a day
before this PR and already claiming that rule ID. Renumbered the rule
file, playbook, tests, and all four compliance framework entries to
AZ-CMP-006.

Also fixes a false-positive gap flagged in review: the rule only
checked for an NSG on the VMSS network interface configuration itself,
missing the case where the NSG is attached at the subnet level
instead. A VMSS whose NIC has no NSG but deploys into a
subnet that does have one was being incorrectly flagged. Now resolves
each network interface configuration's subnet (via the existing
get_virtual_networks() collector, no new collector needed) and treats
either a NIC-level or subnet-level NSG as compliant, matching how
AZ-NET-010 already reads subnet.network_security_group.

Added SOC2 to the rule's own FRAMEWORKS dict (was previously only in
soc2.json, inconsistent with how several other rules, e.g. AZ-KV-006,
already include it directly).

Addresses review feedback from TFT444 and m-khan-97 on #275.

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>

* fix: distinguish unresolved subnet from confirmed no-NSG in AZ-CMP-006

subnet_nsgs.get(subnet_id, False) treated an unresolved subnet
reference (VNet collection failure, missing permissions, or a subnet
ID this scan never saw) identically to a resolved subnet confirmed to
have no NSG, reintroducing the false-positive the previous commit was
meant to fix.

Now distinguishes three states per ip_configuration: resolved+has NSG
(compliant), resolved+no NSG (confirmed non-compliant, HIGH), and
unresolved (indeterminate, LOW), mirroring the confirmed/indeterminate
pattern already established in az_cmp_002.py. Also normalizes subnet
IDs to lowercase before comparison, since Azure resource IDs are
case-insensitive and the two APIs involved (VMSS network profile vs.
VNet subnets) aren't guaranteed to return matching casing.

Adds regression tests for an unresolved subnet (VNet collection
returns empty) and for a differently-cased subnet ID match.

Addresses review feedback from ritiksah141 on #275.

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>

* fix: resolve _subnet_id name collision from rebasing onto AZ-CMP-007

AZ-CMP-007 (merged into dev while this branch was in review) and this
branch's subnet-NSG fix for AZ-CMP-006 each defined their own
_subnet_id() helper with different signatures. Landing them in the
same file after the rebase left two same-named functions, and the
second definition silently shadowed the first, breaking the AZ-CMP-007
subnet-exposure test. Renamed this branch's helper to
_vnet_subnet_id() to remove the collision.

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>

* fix: three real gaps in AZ-CMP-006 flagged by review

- Only ip_configurations that actually carry a public IP are checked
  against subnet NSG state. A non-primary ip_config with no public IP
  is not internet-reachable, so its subnet must not be able to force
  a finding on an otherwise-compliant net_config.
- Removed the break after the first non-compliant net_config. A VMSS
  with several exposed configs now gets one finding per config instead
  of silently hiding every attack surface after the first.
- Indeterminate findings now carry vnets_collected in metadata, so a
  persistent zero across many findings is visible as a VNet-collection
  problem instead of reading as an ordinary per-subnet indeterminate
  result.

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>

* fix: require all public ip_configs protected, not any, in AZ-CMP-006

A net_config can carry several public ip_configs on different
subnets. 'any(status is True for status in subnet_statuses)' treated
the whole net_config as compliant if any one of them was subnet
-protected, even when another public ip_config on the same net_config
was still exposed on an unprotected subnet.

Compliance now requires every public ip_config to be protected. A
confirmed-unprotected ip_config makes the net_config non-compliant
regardless of whether another ip_config on it is merely unresolved,
matching the existing confirmed-beats-indeterminate severity model.

Added a regression test: two public ip_configs on different subnets,
one protected and one not, expecting one confirmed HIGH finding.

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>

---------

Signed-off-by: Dipesh Ray <dipesh.ray.g@gmail.com>
* Add enterprise AKS and workload security controls

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* Address AKS security review feedback

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* Harden AKS tenant evidence isolation

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* Fix trusted registry prefix boundaries

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* Fix AKS policy namespace exclusions

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* Fix Linux dependency lock completeness

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

* Clarify AKS secret remediation guidance

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>

---------

Signed-off-by: ritiksah141 <ritiksah141@gmail.com>
Co-authored-by: Vishnu Ajith <86302373+Vishnu2707@users.noreply.github.com>
Co-authored-by: Muhammad Ibrahim <135441675+m-khan-97@users.noreply.github.com>
* fix(deps): bump pip to 26.2.1 (Dependabot alert #21)

pip < 26.2.0 would incorrectly handle doubly-encoded package URLs from
indexes, allowing a malicious index to serve unexpected packages. This
updates requirements-lock.txt to pin pip==26.2.1 with the correct hash.

Fixes: https://github.com/OWASP/openshield/security/dependabot/21
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(deps): update lock_dependencies.py pip version guard to 26.2.1

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

* fix(deps): upgrade pip-tools to 7.6.1 for pip 26.2.1 compatibility

pip-tools 7.5.3 imports stdlib_pkgs from pip._internal.utils.compat,
which was removed in pip 26.2.1. pip-tools 7.6.1 dropped that internal
dependency (fixed in 7.4.1). Update the source pin in requirements-lock.in,
the compiled hash in requirements-lock.txt, and the version guard in
lock_dependencies.py to keep all three in sync.

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>

---------

Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
TFT444

This comment was marked as off-topic.

@Vishnu2707 Vishnu2707 self-assigned this Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.