Skip to content
Merged
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
5 changes: 5 additions & 0 deletions compliance/frameworks/cis_azure_benchmark.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@
"control_name": "Ensure that 'OS patching' is enabled for virtual machines",
"description": "The virtual machine does not have automatic OS patching enabled. CIS 8.3 requires that OS patches are applied in a timely manner. Unpatched VMs are vulnerable to known exploits targeting unpatched OS vulnerabilities."
},
"AZ-CMP-007": {
"control_id": "N/A-CMP-007",
"control_name": "Just-In-Time (JIT) VM access - Defender for Cloud recommendation, no numbered CIS Azure Foundations 2.0.0 control",
"description": "CIS Microsoft Azure Foundations Benchmark 2.0.0 has no numbered recommendation for Just-In-Time VM access (it is a Microsoft Defender for Cloud recommendation), so under the repository's one-CIS-ID-per-rule convention this rule is not assigned a fabricated control id. It is mapped under NIST CSF PR.AC-3, ISO 27001 A.13.1.1, and SOC 2 CC6.6 instead."
},
"AZ-KV-001": {
"control_id": "N/A-KV-001",
"control_name": "Key Vault soft-delete baseline (covered by the repository's CIS 8.5 purge-protection rule)",
Expand Down
5 changes: 5 additions & 0 deletions compliance/frameworks/iso27001.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@
"control_name": "Management of technical vulnerabilities",
"description": "The virtual machine does not have automatic OS patching enabled. A.12.6.1 requires that information about technical vulnerabilities is obtained and the organisation's exposure evaluated. Without automatic patching, known OS vulnerabilities remain unmitigated."
},
"AZ-CMP-007": {
"control_id": "A.13.1.1",
"control_name": "Network controls",
"description": "A VM has management ports (SSH/RDP) open to the internet with no Just-In-Time VM access policy covering them. A.13.1.1 requires network controls that manage and protect access to systems. JIT limits management-port exposure to approved, time-boxed windows."
},
"AZ-CMP-003": {
"control_id": "A.12.2.1",
"control_name": "Controls against malware",
Expand Down
5 changes: 5 additions & 0 deletions compliance/frameworks/nist_csf.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@
"control_name": "A vulnerability management plan is developed and implemented",
"description": "The virtual machine does not have automatic OS patching enabled. PR.IP-12 requires that a vulnerability management plan is developed and implemented. Without automatic patching, known OS vulnerabilities remain unmitigated and exploitable."
},
"AZ-CMP-007": {
"control_id": "PR.AC-3",
"control_name": "Remote access is managed",
"description": "A VM has management ports (SSH/RDP) open to the internet with no Just-In-Time VM access policy covering them. PR.AC-3 requires that remote access is managed. JIT restricts management-port access to approved, time-boxed requests instead of leaving the ports standing open."
},
"AZ-KV-001": {
"control_id": "PR.IP-4",
"control_name": "Backups of information are conducted, maintained, and tested",
Expand Down
5 changes: 5 additions & 0 deletions compliance/frameworks/soc2.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@
"control_name": "System Vulnerabilities are Identified and Managed",
"description": "The virtual machine does not have automatic OS patching enabled. CC7.1 requires that vulnerabilities in system components are identified and managed through a defined process. Without automatic patching, known OS vulnerabilities are left unmitigated and exploitable."
},
"AZ-CMP-007": {
"control_id": "CC6.6",
"control_name": "Restricts Access from Outside the Network Boundary",
"description": "A VM has management ports (SSH/RDP) open to the internet with no Just-In-Time VM access policy covering them. CC6.6 requires that access from outside the network boundary is restricted. JIT opens management ports only for approved, time-boxed requests instead of continuously."
},
"AZ-KV-001": {
"control_id": "A1.2",
"control_name": "Environmental Threats and Recovery",
Expand Down
39 changes: 39 additions & 0 deletions playbooks/cli/fix_az_cmp_007.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# fix_az_cmp_007.sh
# Enable Microsoft Defender for Cloud Just-In-Time (JIT) VM access so a VM's
# management ports (SSH 22 / RDP 3389) are only opened on approved, time-boxed
# requests instead of standing open to the internet.
#
# The shared 'default' JIT policy is a create-or-update whose PUT replaces the
# whole virtualMachines array, so this script first GETs the current policy and
# merges this VM in (preserving every other VM's JIT config) via
# jit_policy_merge.py before PUTting it back.
#
# Usage: ./fix_az_cmp_007.sh <resource-group> <vm-name> <location>
# Requires: Microsoft Defender for Servers enabled on the subscription.

set -euo pipefail

RESOURCE_GROUP="${1:-}"
VM_NAME="${2:-}"
LOCATION="${3:-}"

if [[ -z "$RESOURCE_GROUP" || -z "$VM_NAME" || -z "$LOCATION" ]]; then
echo "Usage: $0 <resource-group> <vm-name> <location>"
exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SUBSCRIPTION_ID="$(az account show --query id -o tsv)"
VM_ID="/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Compute/virtualMachines/${VM_NAME}"
POLICY_URL="https://management.azure.com/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.Security/locations/${LOCATION}/jitNetworkAccessPolicies/default?api-version=2020-01-01"

echo "Reading existing JIT policy (if any) in $LOCATION..."
EXISTING="$(az rest --method GET --url "$POLICY_URL" 2>/dev/null || true)"

echo "Merging $VM_NAME (ports 22, 3389) into the policy without dropping other VMs..."
BODY="$(printf '%s' "$EXISTING" | python3 "${SCRIPT_DIR}/jit_policy_merge.py" "$VM_ID" 22 3389)"

az rest --method PUT --url "$POLICY_URL" --body "$BODY"

echo "Done. JIT policy 'default' now covers $VM_NAME (existing VMs preserved); management ports require an approved, time-boxed request."
70 changes: 70 additions & 0 deletions playbooks/cli/jit_policy_merge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Merge one VM's JIT entry into an existing Defender for Cloud JIT policy.

The Defender for Cloud ``jitNetworkAccessPolicies`` PUT is a *create-or-update*
that replaces the whole ``properties.virtualMachines`` array. Sending a policy
that contains only the VM being remediated would therefore drop JIT coverage for
every other VM already in the shared ``default`` policy.

This helper reads the current policy JSON from stdin (empty/whitespace means the
policy does not exist yet) and prints the full policy to PUT, preserving every
other VM's entry and replacing (not duplicating) the target VM's own entry.

Usage:
az rest --method GET ... | jit_policy_merge.py <vm-id> <port> [<port> ...]
"""

from __future__ import annotations

import json
import sys
from typing import Any, Dict, List

_DEFAULT_MAX_DURATION = "PT3H"


def build_vm_entry(vm_id: str, ports: List[int]) -> Dict[str, Any]:
return {
"id": vm_id,
"ports": [
{
"number": port,
"protocol": "*",
"allowedSourceAddressPrefix": "*",
"maxRequestAccessDuration": _DEFAULT_MAX_DURATION,
}
for port in ports
],
}


def merge_policy(existing: Dict[str, Any] | None, vm_entry: Dict[str, Any]) -> Dict[str, Any]:
"""Return a policy that keeps every other VM and (re)sets the target VM's entry."""
if existing and isinstance(existing.get("properties"), dict):
policy = existing
else:
policy = {"kind": "Basic", "properties": {}}
properties = policy.setdefault("properties", {})
others = [
vm
for vm in (properties.get("virtualMachines") or [])
if isinstance(vm, dict) and vm.get("id") != vm_entry["id"]
]
properties["virtualMachines"] = others + [vm_entry]
return policy


def main(argv: List[str]) -> int:
if len(argv) < 3:
print(f"usage: {argv[0]} <vm-id> <port> [<port> ...]", file=sys.stderr)
return 2
vm_id = argv[1]
ports = [int(port) for port in argv[2:]]
raw = sys.stdin.read().strip()
existing = json.loads(raw) if raw else None
print(json.dumps(merge_policy(existing, build_vm_entry(vm_id, ports))))
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv))
17 changes: 17 additions & 0 deletions scanner/azure_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,23 @@ def get_web_apps(self) -> List[Any]:
logger.error("get_web_apps failed: %s", exc)
return []

def get_jit_network_access_policies(self) -> Optional[List[Any]]:
"""List Microsoft Defender for Cloud Just-In-Time VM access policies.

Returns a list (including an empty list) when Defender for Cloud responds,
or ``None`` when permissions, networking, the SDK, or a subscription
without Defender for Cloud prevent the collection from being evaluated.
Callers must treat ``None`` as "JIT coverage unknown", never as "no JIT".
"""
try:
from azure.mgmt.security import SecurityCenter

client = SecurityCenter(self.credential, self.subscription_id)
return list(client.jit_network_access_policies.list())
except Exception as exc:
logger.error("get_jit_network_access_policies failed: %s", exc)
return None

def get_function_app_security_posture(self) -> Optional[List[Dict[str, Any]]]:
"""Return a cached, secret-free posture for Function Apps."""
if self._function_apps_cache is not _UNSET:
Expand Down
Loading
Loading