Skip to content

feat(core): v0.5.0 - rule evaluations, scan hardening, AI foundations, enterprise rules, and OWASP migration - #346

Closed
Vishnu2707 wants to merge 184 commits into
mainfrom
dev
Closed

Vishnu2707 wants to merge 184 commits into
mainfrom
dev

Conversation

@Vishnu2707

Copy link
Copy Markdown
Collaborator

What does this PR do?

Merges 149 commits from dev into main covering the full v0.5.0 release —
rule evaluations per rule per resource, scan durability and idempotency
hardening, enterprise rule packs, AI layer foundations, frontend
accessibility and i18n, signed releases, supply chain security, OWASP
migration, 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
Depends on #321, #325 already merged to dev

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
SHAURYAKSHARMA24 and others added 23 commits August 23, 2026 02:53
Signed-off-by: Shaurya K Sharma <shauryaksharma24@gmail.com>
Signed-off-by: Shaurya K Sharma <shauryaksharma24@gmail.com>
* feat(frontend): add recoverable page error states

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

* fix(frontend): prevent stale duplicate page loads

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

---------

Signed-off-by: Shaurya K Sharma <shauryaksharma24@gmail.com>
Signed-off-by: m-khan-97 <ibrahim.logix@gmail.com>
…flow-log evidence (#316)

AZ-NET-012 called AzureClient.get_nsg_flow_logs(resource_group), which does
not exist. Its bare except turned that AttributeError into a finding for
every NSG in production, and the test suite mocked the invented method
instead of catching the bug.

The remediation script also only ever created NSG flow logs. Microsoft
stopped new NSG flow log creation on 2025-06-30 and retires the feature
entirely on 2027-09-30, so recommending new NSG flow log creation is
itself broken guidance.

- AzureClient.get_flow_logs() lists Network Watcher flow logs per-region
  (there is no flat subscription-wide endpoint), returning a dict keyed by
  normalized region. A region maps to its list of FlowLog resources, None
  when that region's watcher/flow-log listing failed, or is simply absent
  when no watcher exists there - callers must treat all three as
  indeterminate, never as "no flow log configured". A failure on one
  region's watcher does not discard results already collected from
  another region.
- az_net_012.scan() now evaluates VNet-scoped flow logs at the correct
  Azure scope. An existing (never newly created) legacy NSG flow log on
  one of the VNet's own subnet NSGs is accepted as coverage rather than
  driving a migration finding, since NSG flow logs remain functional
  until 2027 and creating a new one is blocked. A region with no
  reachable Network Watcher evidence is skipped, not flagged - a missing
  watcher is AZ-NET-011's finding, not this rule's.
- playbooks/cli/fix_az_net_012.sh now creates a VNet flow log instead of
  an NSG flow log, and is idempotent (checks for an already-enabled flow
  log first), preview-first (prints the exact command before requiring
  APPLY confirmation), and target-verified (re-reads state after
  creation, prints the rollback command on verification failure).
- Framework mapping descriptions (all four compliance JSONs),
  docs/rules-reference.md, and website/content.js updated to describe the
  VNet flow-log evidence semantics instead of NSG-only.

Tests: replaced the two tests that validated the rule against a mocked
method the real client never implemented with nine tests covering no
VNets, VNet flow log compliant/noncompliant, legacy NSG flow log
compliant/noncompliant, an unwatched region, a failed region, and partial
collection (one region fails without suppressing a real finding in
another). Added three unit tests for AzureClient.get_flow_logs() directly
covering the multi-region merge, total failure, and per-region partial
failure.

Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
…6-45833) (#317)

* fix(deps): move chromadb to optional requirements-ai.txt (CVE-2026-45830, CVE-2026-45833)

Both CVEs affect chromadb >= 0.4.24 with no patch available. Remove the
package from requirements.txt so the core install and Docker image are no
longer exposed. The ai/ RAG pipeline already handles ImportError gracefully,
so the feature continues to work when requirements-ai.txt is installed
separately. Add a regression test asserting chromadb never re-enters the
core requirements file.

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

* fix(deps): pin six explicitly and fix E501 in RAG test

azure-mgmt-rdbms==10.1.0 requires six, which was previously satisfied
transitively by chromadb. Pin it directly now that chromadb is no longer
in core requirements. Also wrap the pytest.importorskip call to stay within
the 120-character line limit.

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

* fix(deps): wire AI deps opt-in to Dockerfile, restore six for rdbms

- Add INSTALL_AI_DEPS build arg (default false); chromadb excluded by
  default due to CVE-2026-45830/45833 with no upstream fix. AI routes
  return 503 without it; build with --build-arg INSTALL_AI_DEPS=true to
  enable the RAG pipeline.
- Restore six>=1.16.0: azure-mgmt-rdbms 10.1.0 imports six internally
  but does not declare it as a metadata dependency, so pip does not
  install it transitively. Reverting the strict version pin to a floor
  constraint satisfies the reviewer concern while keeping CI green.
- Strip UTF-8 BOM from ai/README.md (Windows editor artifact).

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

* fix(ai): replace chromadb with pure-Python BM25 retrieval

Removes chromadb entirely (CVE-2026-45830 tenant-isolation bypass,
CVE-2026-45833 RCE) and replaces the vector store with a JSON BM25
index built from the same loader/chunker pipeline.

- ai/embed.py: tokenise chunks, compute IDF, write bm25_index.json
  atomically via a .tmp swap so a failed build never corrupts the index
- ai/retriever.py: load index and score with BM25 (k1=1.5, b=0.75);
  VectorStoreNotBuilt exception preserved for API route compatibility
- requirements-ai.txt: deleted; no optional install step needed
- Dockerfile: removed INSTALL_AI_DEPS build-arg complexity; single
  pip install -r requirements.txt is sufficient
- ai/README.md: documents the BM25 approach and build command
- tests/test_rag_dependencies.py: replaces skipped chromadb test with
  8 BM25 unit tests covering tokeniser, scoring, and absence checks

The retrieve() interface is unchanged: callers receive the same
list of {text, source, source_meta} dicts.

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

* fix(ai): validate Dockerfile and BM25 retrieval

Remove the UTF-8 BOM that made the Dockerfile's first instruction invalid. Add regression coverage for the Dockerfile byte boundary and for building and querying the dependency-free BM25 index with representative OpenShield content.

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

* fix(ai): initialize and validate BM25 index

Build the dependency-free index into every production image and smoke-test retrieval from that fresh image in CI. Validate the persisted schema before scoring so malformed indexes fail through VectorStoreNotBuilt. Ensure chunking always advances when newlines fall inside the overlap window.

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

---------

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

* feat: add rule AZ-CMP-007 management ports open without JIT VM access

Adds a Compute scan rule that flags VMs whose management ports (SSH 22 /
RDP 3389) are open to the internet through their NIC's NSG but are not
covered by a Microsoft Defender for Cloud Just-In-Time (JIT) VM access
policy, leaving those ports standing open instead of only during an
approved, time-boxed request.

Detection reuses the repo's proven building blocks: az_net_001's
"allow inbound port from any source" test and az_cmp_001's VM -> NIC -> NSG
resolution, cross-referenced against JIT policies from a new
AzureClient.get_jit_network_access_policies() getter (Defender for Cloud
SecurityCenter SDK; azure-mgmt-security is already a dependency). VMs with
no management ports open are NOT_APPLICABLE, and when Defender for Cloud
cannot be queried (None) coverage is indeterminate and the VM is not
flagged, so the rule never false-positives on unknown coverage.

Includes the remediation playbook (playbooks/cli/fix_az_cmp_007.sh, with
guarded args per the fix_az_net_016.sh convention), mock support
(set_jit_policies / get_jit_network_access_policies), six unit tests
(open-no-JIT, JIT-covered, NOT_APPLICABLE, trusted-source, partial-coverage,
indeterminate), and mappings across the four compliance frameworks
(NIST CSF PR.AC-3, ISO 27001 A.13.1.1, SOC 2 CC6.6; CIS uses the repo's N/A
convention as Azure Foundations 2.0.0 has no numbered JIT control).

Closes #270

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

* fix(AZ-CMP-007): port ranges, subnet NSGs, and JIT-preserving remediation

Addresses @ritiksah141's review on #307:

- Port ranges: _rule_allows_port_from_any now parses '*', single ports, and
  inclusive ranges (e.g. 20-30 exposing SSH) in both destination_port_range and
  destination_port_ranges, instead of only exact/'*' matches.
- Subnet-level NSGs: exposure is evaluated across every NSG that governs the VM
  -- the NIC's NSG and the NSG on the NIC IP configuration's subnet (resolved via
  each NSG's own .subnets back-reference) -- so a VM with no NIC-level NSG but an
  exposing subnet NSG is no longer a false NOT_APPLICABLE.
- Remediation: fix_az_cmp_007.sh now GETs the existing 'default' JIT policy and
  merges this VM in via jit_policy_merge.py (preserving every other VM's entry)
  before the create-or-update PUT, instead of overwriting the shared policy with
  a single VM.

Adds regression tests: 22 inside a range, 3389 inside a ranges-list entry, a
subnet-only NSG exposure case, and jit_policy_merge tests proving an existing
multi-VM policy is preserved (and that re-remediation updates in place).

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

---------

Signed-off-by: shariqueahmad108-ship-it <shariqueahmad108@gmail.com>
* fix(core): enforce severity contract v1

Signed-off-by: m-khan-97 <ibrahim.logix@gmail.com>

* fix(core): make scan persistence atomic

Signed-off-by: m-khan-97 <ibrahim.logix@gmail.com>

* fix(sentinel): require explicit finding severity

Signed-off-by: m-khan-97 <ibrahim.logix@gmail.com>

---------

Signed-off-by: m-khan-97 <ibrahim.logix@gmail.com>
* 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>
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.