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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ GEMINI_API_KEY=
# Optional
NVD_API_KEY=
SENTRY_DSN=

# Optional - enables AZ-SC-007/008 (Azure DevOps pipeline scanning)
AZURE_DEVOPS_ORG_URL=
AZURE_DEVOPS_PROJECT=
40 changes: 19 additions & 21 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,9 @@ from typing import Any, Dict, List

RULE_ID = "AZ-STOR-001"
RULE_NAME = "Public Blob Access Enabled on Storage Account"
SEVERITY = "HIGH" # HIGH / MEDIUM / LOW / INFO
CATEGORY = "Storage" # Storage / Network / Identity / Database / Compute / Key Vault / Kubernetes
FRAMEWORKS = {
"CIS": "3.5",
"NIST": "PR.AC-3",
"ISO27001": "A.9.4.1"
}
SEVERITY = "HIGH" # HIGH / MEDIUM / LOW / INFO
CATEGORY = "Storage" # Storage / Network / Identity / Database / Compute / Key Vault / Kubernetes
FRAMEWORKS = {"CIS": "3.5", "NIST": "PR.AC-3", "ISO27001": "A.9.4.1"}
DESCRIPTION = (
"Storage accounts with public blob access enabled allow anyone on the "
"internet to read data without authentication. This can lead to data "
Expand All @@ -72,20 +68,22 @@ def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]:

for account in azure_client.get_storage_accounts():
if getattr(account, "allow_blob_public_access", False):
findings.append({
"rule_id": RULE_ID,
"rule_name": RULE_NAME,
"severity": SEVERITY,
"category": CATEGORY,
"resource_id": account.id,
"resource_name": account.name,
"resource_type": "Microsoft.Storage/storageAccounts",
"description": DESCRIPTION,
"remediation": REMEDIATION,
"playbook": PLAYBOOK,
"frameworks": FRAMEWORKS,
"metadata": {}
})
findings.append(
{
"rule_id": RULE_ID,
"rule_name": RULE_NAME,
"severity": SEVERITY,
"category": CATEGORY,
"resource_id": account.id,
"resource_name": account.name,
"resource_type": "Microsoft.Storage/storageAccounts",
"description": DESCRIPTION,
"remediation": REMEDIATION,
"playbook": PLAYBOOK,
"frameworks": FRAMEWORKS,
"metadata": {},
}
)

return findings
```
Expand Down
40 changes: 40 additions & 0 deletions compliance/frameworks/cis_azure_benchmark.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,46 @@
"control_id": "TBD-IDN-015",
"control_name": "Managed Identity least privilege (not mapped in CIS Azure Foundations 2.0.0)",
"description": "Microsoft recommends least-privilege roles and scopes for managed identities. This check has no direct control in the repository's CIS Azure Foundations 2.0.0 benchmark."
},
"AZ-SC-001": {
"control_id": "TBD-SC-001",
"control_name": "Container Registry Admin User Enabled placeholder",
"description": "Numbered placeholder pending maintainer approval of a direct CIS mapping."
},
"AZ-SC-002": {
"control_id": "TBD-SC-002",
"control_name": "Container Registry Public Network Access Enabled placeholder",
"description": "Numbered placeholder pending maintainer approval of a direct CIS mapping."
},
"AZ-SC-003": {
"control_id": "TBD-SC-003",
"control_name": "Container Registry Allows Anonymous Pull placeholder",
"description": "Numbered placeholder pending maintainer approval of a direct CIS mapping."
},
"AZ-SC-004": {
"control_id": "TBD-SC-004",
"control_name": "Container Registry Missing Retention or Quarantine Policy placeholder",
"description": "Numbered placeholder pending maintainer approval of a direct CIS mapping."
},
"AZ-SC-005": {
"control_id": "TBD-SC-005",
"control_name": "Terraform State Storage Container Publicly Readable placeholder",
"description": "Numbered placeholder pending maintainer approval of a direct CIS mapping."
},
"AZ-SC-006": {
"control_id": "TBD-SC-006",
"control_name": "Terraform State Storage Account Missing Versioning or Soft Delete placeholder",
"description": "Numbered placeholder pending maintainer approval of a direct CIS mapping."
},
"AZ-SC-007": {
"control_id": "TBD-SC-007",
"control_name": "Pipeline Service Connection Scoped to Subscription placeholder",
"description": "Numbered placeholder pending maintainer approval of a direct CIS mapping."
},
"AZ-SC-008": {
"control_id": "TBD-SC-008",
"control_name": "Pipeline Service Connection Uses Password Instead of Federated Credential placeholder",
"description": "Numbered placeholder pending maintainer approval of a direct CIS mapping."
}
}
}
40 changes: 40 additions & 0 deletions compliance/frameworks/iso27001.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,46 @@
"control_id": "A.9.2.3",
"control_name": "Management of privileged access rights",
"description": "Subscription Owner and Contributor assignments to managed identities require least-privilege reduction."
},
"AZ-SC-001": {
"control_id": "A.9.2.1",
"control_name": "User registration and de-registration",
"description": "The Container Registry admin user is enabled, providing a shared credential that bypasses individual identity management and cannot be attributed to a single user."
},
"AZ-SC-002": {
"control_id": "A.13.1.1",
"control_name": "Network controls",
"description": "The Container Registry is reachable from the public internet, leaving the network boundary that protects the organization's built container images uncontrolled."
},
"AZ-SC-003": {
"control_id": "A.9.2.1",
"control_name": "User registration and de-registration",
"description": "The Container Registry allows anonymous pull, letting any client access every image without an authenticated, individually attributable identity."
},
"AZ-SC-004": {
"control_id": "A.12.1.2",
"control_name": "Change management",
"description": "The Container Registry has no retention or quarantine policy, so stale images accumulate and newly pushed images are deployable before any vulnerability scan evaluates them."
},
"AZ-SC-005": {
"control_id": "A.13.1.1",
"control_name": "Network controls",
"description": "A Terraform remote state container is publicly readable, leaving the network boundary around infrastructure layout and captured secrets uncontrolled."
},
"AZ-SC-006": {
"control_id": "A.12.3.1",
"control_name": "Information backup",
"description": "A storage account holding Terraform remote state has neither versioning nor soft delete enabled, so an overwritten or deleted state file cannot be recovered."
},
"AZ-SC-007": {
"control_id": "A.9.2.3",
"control_name": "Management of privileged access rights",
"description": "A pipeline service connection is scoped to the entire subscription rather than a single resource group, so every pipeline that uses it inherits subscription-wide access beyond what it needs."
},
"AZ-SC-008": {
"control_id": "A.9.4.3",
"control_name": "Password management system",
"description": "A pipeline service connection authenticates with a stored service principal secret instead of a federated credential, leaving a static credential to rotate and potentially leak."
}
}
}
40 changes: 40 additions & 0 deletions compliance/frameworks/nist_csf.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,46 @@
"control_id": "PR.AC-4",
"control_name": "Access permissions and authorizations are managed",
"description": "Managed identities should receive only the minimum role and scope required by their workloads."
},
"AZ-SC-001": {
"control_id": "PR.AC-1",
"control_name": "Identities and credentials are issued, managed, verified, revoked, and audited",
"description": "The Container Registry admin user is enabled, providing a shared credential that bypasses individual identity management and cannot be attributed to a single user."
},
"AZ-SC-002": {
"control_id": "PR.AC-5",
"control_name": "Network integrity is protected",
"description": "The Container Registry is reachable from the public internet, leaving the network boundary that protects the organization's built container images uncontrolled."
},
"AZ-SC-003": {
"control_id": "PR.AC-1",
"control_name": "Identities and credentials are issued, managed, verified, revoked, and audited",
"description": "The Container Registry allows anonymous pull, letting any client access every image without an authenticated, individually attributable identity."
},
"AZ-SC-004": {
"control_id": "PR.IP-1",
"control_name": "A baseline configuration is created and maintained",
"description": "The Container Registry has no retention or quarantine policy, so stale images accumulate and newly pushed images are deployable before any vulnerability scan evaluates them."
},
"AZ-SC-005": {
"control_id": "PR.AC-5",
"control_name": "Network integrity is protected",
"description": "A Terraform remote state container is publicly readable, leaving the network boundary around infrastructure layout and captured secrets uncontrolled."
},
"AZ-SC-006": {
"control_id": "PR.IP-4",
"control_name": "Backups of information are conducted, maintained, and tested",
"description": "A storage account holding Terraform remote state has neither versioning nor soft delete enabled, so an overwritten or deleted state file cannot be recovered."
},
"AZ-SC-007": {
"control_id": "PR.AC-4",
"control_name": "Access permissions and authorizations are managed",
"description": "A pipeline service connection is scoped to the entire subscription rather than a single resource group, so every pipeline that uses it inherits subscription-wide access beyond what it needs."
},
"AZ-SC-008": {
"control_id": "PR.AC-1",
"control_name": "Identities and credentials are issued, managed, verified, revoked, and audited",
"description": "A pipeline service connection authenticates with a stored service principal secret instead of a federated credential, leaving a static credential to rotate and potentially leak."
}
}
}
40 changes: 40 additions & 0 deletions compliance/frameworks/soc2.json
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,46 @@
"control_id": "CC6.3",
"control_name": "Role-Based Access",
"description": "Managed identities should not receive broad subscription roles beyond workload requirements."
},
"AZ-SC-001": {
"control_id": "CC6.1",
"control_name": "Logical Access Security Measures",
"description": "The Container Registry admin user is enabled, providing a shared credential that bypasses individual identity management and cannot be attributed to a single user."
},
"AZ-SC-002": {
"control_id": "CC6.6",
"control_name": "Restricts Access from Outside the Network Boundary",
"description": "The Container Registry is reachable from the public internet, leaving the network boundary that protects the organization's built container images uncontrolled."
},
"AZ-SC-003": {
"control_id": "CC6.1",
"control_name": "Logical Access Security Measures",
"description": "The Container Registry allows anonymous pull, letting any client access every image without an authenticated, individually attributable identity."
},
"AZ-SC-004": {
"control_id": "CC7.1",
"control_name": "System Vulnerabilities are Identified and Managed",
"description": "The Container Registry has no retention or quarantine policy, so stale images accumulate and newly pushed images are deployable before any vulnerability scan evaluates them."
},
"AZ-SC-005": {
"control_id": "CC6.6",
"control_name": "Restricts Access from Outside the Network Boundary",
"description": "A Terraform remote state container is publicly readable, leaving the network boundary around infrastructure layout and captured secrets uncontrolled."
},
"AZ-SC-006": {
"control_id": "A1.2",
"control_name": "Environmental Threats and Recovery",
"description": "A storage account holding Terraform remote state has neither versioning nor soft delete enabled, so an overwritten or deleted state file cannot be recovered."
},
"AZ-SC-007": {
"control_id": "CC6.1",
"control_name": "Logical Access Security Measures",
"description": "A pipeline service connection is scoped to the entire subscription rather than a single resource group, so every pipeline that uses it inherits subscription-wide access beyond what it needs."
},
"AZ-SC-008": {
"control_id": "CC6.1",
"control_name": "Logical Access Security Measures",
"description": "A pipeline service connection authenticates with a stored service principal secret instead of a federated credential, leaving a static credential to rotate and potentially leak."
}
}
}
56 changes: 32 additions & 24 deletions docs/adding-a-rule.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,26 @@ logger = logging.getLogger(__name__)

# ── Required module-level constants ─────────────────────────────────────────

RULE_ID = "AZ-XXXX-000" # Unique ID. Check existing rules to avoid clashes.
RULE_NAME = "Human-readable name" # Shown in the dashboard and reports.
SEVERITY = "HIGH" # HIGH | MEDIUM | LOW | INFO
CATEGORY = "Storage" # Storage | Network | Identity | Database | Compute | Key Vault | Kubernetes
RULE_ID = "AZ-XXXX-000" # Unique ID. Check existing rules to avoid clashes.
RULE_NAME = "Human-readable name" # Shown in the dashboard and reports.
SEVERITY = "HIGH" # HIGH | MEDIUM | LOW | INFO
CATEGORY = "Storage" # Storage | Network | Identity | Database | Compute | Key Vault | Kubernetes
FRAMEWORKS = {
"CIS": "3.5", # CIS Azure Benchmark control ID
"NIST": "PR.AC-3", # NIST CSF subcategory
"ISO27001": "A.9.4.1", # ISO 27001 Annex A control
"CIS": "3.5", # CIS Azure Benchmark control ID
"NIST": "PR.AC-3", # NIST CSF subcategory
"ISO27001": "A.9.4.1", # ISO 27001 Annex A control
}
DESCRIPTION = (
"Explain WHY this is a security risk. One or two sentences. "
"What can an attacker do if this misconfiguration exists?"
)
REMEDIATION = (
"Explain HOW to fix it. What setting to change, or what command to run."
)
REMEDIATION = "Explain HOW to fix it. What setting to change, or what command to run."
PLAYBOOK = "playbooks/cli/fix_az_xxxx_000.sh" # path to the matching fix script


# ── Required scan function ───────────────────────────────────────────────────


def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]:
"""Return a list of findings. Return [] if no issues are found.

Expand Down Expand Up @@ -74,20 +73,22 @@ def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]:
continue

if status is False:
findings.append({
"rule_id": RULE_ID,
"rule_name": RULE_NAME,
"severity": SEVERITY,
"category": CATEGORY,
"resource_id": resource_id,
"resource_name": resource_name,
"resource_type": "Microsoft.Storage/storageAccounts", # ← update
"description": DESCRIPTION,
"remediation": REMEDIATION,
"playbook": PLAYBOOK,
"frameworks": FRAMEWORKS,
"metadata": {},
})
findings.append(
{
"rule_id": RULE_ID,
"rule_name": RULE_NAME,
"severity": SEVERITY,
"category": CATEGORY,
"resource_id": resource_id,
"resource_name": resource_name,
"resource_type": "Microsoft.Storage/storageAccounts", # ← update
"description": DESCRIPTION,
"remediation": REMEDIATION,
"playbook": PLAYBOOK,
"frameworks": FRAMEWORKS,
"metadata": {},
}
)

return findings
```
Expand Down Expand Up @@ -130,12 +131,19 @@ def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]:
| `azure_client.get_subscription_role_assignments()` | Subscription RBAC assignments, or `None` on API failure |
| `azure_client.get_service_principals()` | List of RoleAssignment objects for service principals |
| `azure_client.get_conditional_access_policies()` | List of CA policy dicts from MS Graph |
| `azure_client.get_container_registries()` | List of ACR Registry objects, or `None` on API failure |
| `azure_client.get_blob_containers(rg, account)` | List of blob container items (with `public_access`), or `None` on API failure |
| `azure_client.get_blob_service_properties(rg, account)` | BlobServiceProperties (versioning, soft delete), or `None` on API failure |
| `azure_client.devops_client` | `DevOpsClient` instance, or `None` if `AZURE_DEVOPS_ORG_URL`/`AZURE_DEVOPS_PROJECT` are not configured |
| `azure_client.devops_client.get_service_endpoints()` | List of Azure DevOps service connections, or `None` on API failure |
| `azure_client.parse_resource_id(id)` | Dict with `resource_group` and `name` |

List methods return an empty list on failure. Single-resource methods return `None` when the resource cannot be fetched. Three-state checks, such as `get_storage_lifecycle_policy()`, return `True` for compliant, `False` for non-compliant, and `None` when the scanner cannot determine the state.

When a helper returns `None`, skip the resource and log a warning. Never create a finding from an unknown state.

`azure_client.devops_client` is `None` whenever Azure DevOps is not configured for the scanned subscription — treat that the same as "not applicable" and return no findings, not as an indeterminate failure.

---

## Write the Remediation Playbook
Expand Down
Loading
Loading