fix(scanner): remove compute-rule false positives/negatives (#268) - #272
fix(scanner): remove compute-rule false positives/negatives (#268)#272parthrohit22 wants to merge 9 commits into
Conversation
21001ff to
465d07b
Compare
TFT444
left a comment
There was a problem hiding this comment.
@parthrohit22 Good improvements, but this does not yet complete issue #268. Defender health and real patch-assessment evidence are still missing, and an inaccessible subnet can currently be treated as confirmed unprotected and produce a HIGH finding. Please either implement the remaining scope and return an unknown result for unreadable subnet evidence, or narrow the PR and linked issue to the work actually completed.
1cad174 to
eb66ca9
Compare
|
@TFT444 - I can see a new commit, please do review the PR again. @parthrohit22 - as a best practise it would be great if u could put a comment on what was addressed. |
@parthrohit22 has not pushed any updates since my review. It needs either the remaining scope implemented plus the subnet unknown result fix or the PR and linked issue narrowed to match what's actually done, plus conflict resolution, before this can merge. What's blocking it: Your review said the PR doesn't fully complete issue #268. Defender health and real patch-assessment evidence are still missing. @parthrohit22 please have a look into the conflict and the blocking i mentioned please |
875c7c2 to
dd1a16f
Compare
Ready for reviewPR #272 has been updated with the review follow-up:
Validation completed:
Review commits: |
TFT444
left a comment
There was a problem hiding this comment.
@parthrohit22 re-reviewed the current head. All three blockers from my last review are genuinely fixed: Defender health is now the primary signal in AZ-CMP-003, AZ-CMP-004 checks real patch-assessment evidence, and unresolvable subnets in AZ-CMP-001 surface as indeterminate LOW instead of confirmed HIGH. Test coverage is strong, including the SDK-shape guard.
One thing before merge: the PR description is stale. It still says az_cmp_004.py is unchanged (the diff modifies it substantially) and never mentions the Defender integration or the new azure-mgmt-security==7.0.0 dependency. Please rewrite it to match the head, since a new third-party dependency should not be invisible in the description.
Non-blocking follow-ups: _defender_endpoint_protection_status scans the full subscription assessment list per VM (consider indexing by resource ID once), and the loose "endpoint protection" in display_name match can hit multiple assessment types order-dependently.
Update the description and I'm happy to dissolve my block.
|
@TFT444 Updated:
Latest commit: |
b9d0b6a to
3d1537c
Compare
SHAURYAKSHARMA24
left a comment
There was a problem hiding this comment.
Requesting changes.
Thanks for addressing the earlier review points. I re-reviewed the current head and found four remaining correctness issues:
-
AZ-CMP-001: an indeterminate public NIC causes an immediatebreak, so a later NIC with a confirmed public-IP/no-NSG exposure is never evaluated. A confirmed HIGH finding can therefore be downgraded to LOW. -
AZ-CMP-003: Defender matching only accepts display names containingendpoint protection. This misses the current Defender recommendation,EDR solution should be installed on virtual machines, causing Healthy Defender evidence to be ignored and potentially producing false HIGH findings through extension fallback. -
AZ-CMP-003: an extension with missingprovisioning_stateis treated as healthy and silently passes. Missing state is unknown evidence and should be surfaced as indeterminate rather than compliant. -
AZ-CMP-004: unavailable, failed, incomplete, and stale patch assessments are treated as clean whenever automatic patching is configured. The rule needs an indeterminate result for unavailable/non-conclusive/stale evidence, and should uselast_modified_timeto enforce a freshness threshold.
Please add regression coverage for each scenario and request re-review after updating the PR.
3d1537c to
c36673f
Compare
|
@SHAURYAKSHARMA24 All four addressed in c36673f:
Regression coverage: Please take another look when you get a chance. |
…01/003) AZ-CMP-001: a VM's NIC lacking its own NSG is no longer flagged if the NIC's subnet carries a protecting NSG instead (a valid, common Azure pattern). Added AzureClient.get_subnet() to resolve the subnet referenced by a NIC's ip_configuration; an unresolvable subnet is still treated as unprotected so this only removes false positives, never introduces false negatives. AZ-CMP-003: a recognised endpoint-protection extension whose provisioning_state is present and not "Succeeded" is no longer read as a silent pass. It now surfaces as an indeterminate/LOW finding instead, mirroring AZ-CMP-002's existing determination convention. Extensions with no provisioning_state data (not exposed by the API) still fall back to the prior name-based check to avoid inventing new false positives. AZ-CMP-004: verified against current code -- it already checks patch_mode == "AutomaticByPlatform" for both Windows and Linux, so the weak-signal claim in openshield-org#268 does not apply here. No change made. Fixes openshield-org#268 Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>
…not confirmed HIGH An unresolvable subnet (permissions gap, transient API error, deleted resource) said nothing about whether it actually has an NSG, but was being folded into "no subnet NSG" and reported as a confirmed HIGH finding. Split subnet resolution into a tri-state result (protected / confirmed unprotected / unknown) and report the unknown case as a LOW indeterminate finding instead, matching the confirmed/indeterminate convention already used by AZ-CMP-002. Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>
…(AZ-CMP-003/004) Completes the remaining scope of openshield-org#268: both rules previously relied on weak signals that could produce false negatives. AZ-CMP-003 only checked whether a named endpoint-protection extension was installed, with no health check - an installed-but-broken AV/EDR agent still read as protected. It now queries Microsoft Defender for Cloud's "Endpoint protection" security assessment as the primary signal (real agent health telemetry) and falls back to the existing extension-name/provisioning-state check only when Defender data is unavailable (not onboarded, no assessment yet, API failure). AZ-CMP-004 only read config flags (enable_automatic_updates / patch_mode), never actual patch compliance - a VM could look compliant by config while being months behind on real patches. It now also fetches the VM's live patch assessment (Azure Update Manager / Microsoft.Maintenance data, via the instance view's patch_status) and flags a confirmed violation when a completed assessment shows pending critical/security patches, even if config looks fine. A clean or unavailable assessment never suppresses an existing config-based finding, since disabled auto-patching is itself a real drift risk regardless of today's point-in-time patch level. Adds AzureClient.get_security_assessments() (Microsoft Defender for Cloud, new azure-mgmt-security dependency) and AzureClient.get_vm_patch_status() (azure-mgmt-compute instance view, no new dependency), both fail-closed (None) on API/permission errors so callers never mistake "signal unavailable" for compliant. Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>
Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>
Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>
…Z-CMP-003) _defender_endpoint_protection_status previously rescanned the full subscription-wide assessments list for every VM. Build a resource-ID index once per scan() call instead, and make the match deterministic: when a resource has more than one "endpoint protection" assessment, an Unhealthy code always wins regardless of API response order. Signed-off-by: PARTH J ROHIT <parthrohit60@gmail.com>
AZ-CMP-001: an indeterminate result on one NIC caused an immediate break, so a later NIC on the same VM with a confirmed public-IP/no-NSG exposure was never evaluated - a real HIGH could be silently downgraded to LOW just because of NIC iteration order. scan() now keeps the worst evaluated result across all of a VM's NICs, only stopping early once a confirmed violation is found (nothing can outrank it). AZ-CMP-003: - Defender's "Endpoint protection should be installed" recommendation was renamed to "EDR solution should be installed on virtual machines" when Microsoft moved from the deprecated Log Analytics agent to agentless EDR scanning. Matching only "endpoint protection" meant the index silently matched nothing against a current subscription's real data, so Defender's signal was never found and every VM fell back to the weaker extension check. Now matches either display name. - An extension with a missing provisioning_state was treated the same as "Succeeded" (confirmed healthy). Missing state is unknown evidence, not proof of success - it's now folded into the same indeterminate path as Failed/Canceled instead of silently passing. AZ-CMP-004: unavailable, non-conclusive (e.g. InProgress), and stale patch assessments were all treated as a clean pass whenever config-based patching was enabled - config alone was silently treated as sufficient even with no real, current evidence backing it. The assessment check now requires a conclusive status AND a last_modified_time within a 30-day freshness threshold to count as a genuine clean signal; anything short of that (unavailable, non-conclusive, or stale) surfaces as an indeterminate LOW finding instead of a silent pass, mirroring the LOW/indeterminate split AZ-CMP-001/003 already use for their own unresolvable evidence. Added regression coverage for all four: two NIC-ordering cases for AZ-CMP-001, the current EDR display name and missing-provisioning-state cases for AZ-CMP-003, and unavailable/non-conclusive/stale/fresh/missing- timestamp cases for AZ-CMP-004. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
c36673f to
47403e5
Compare
…e-rule-quality # Conflicts: # tests/helpers/mock_azure.py # tests/test_rules_compute.py
The dev-merge commit (6aab017) resolved a *textual* conflict in tests/test_rules_compute.py cleanly but left a *semantic* one: this branch's own _subnet_id(vnet_name, subnet_name) helper (10 call sites, used throughout the AZ-CMP-001 tests) and dev's newly-added _subnet_id(name) helper (added for the AZ-CMP-007 JIT tests, 1 call site) share a name in two non-adjacent parts of the file, so git saw no textual overlap - but Python resolves both definitions to whichever one appears last in the module, silently shadowing this branch's 2-arg version for every one of its 10 call sites. Renamed dev's newer, single-use helper to _jit_subnet_id instead of touching the 10 existing call sites. Caught by actually running the test suite after the merge, not just checking for a clean git merge. Signed-off-by: Parth J Rohit <parthrohit60@gmail.com> Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
|
Rebased onto current Two real files conflicted ( Verified: full backend suite (812 passed, 5 skipped — pre-existing/environment-only), ruff and format clean. All 20 CI checks are green on the current head. @SHAURYAKSHARMA24 @TFT444 all four items from your reviews were addressed in |
Dismissing to allow deeper review before any approval decision
There was a problem hiding this comment.
AZ-CMP-003: Silent false-negative on multi-extension VMs break on first succeeded extension skips all remaining extensions, leaving failed ones undetected and unconfirmed_names incomplete. A VM with IaaSAntimalware (succeeded) + MDE.Linux (failed) is stamped compliant; the failed extension never appears in finding metadata — directly contradicting the Defender 'unhealthy wins' contract already enforced in this same rule.
AZ-CMP-003: Overly broad "edr solution" substring match creates future false-negative surface. Any new Microsoft Defender recommendation containing 'edr solution' as a substring will be incorrectly associated with the endpoint-protection check; tightening to "edr solution should be installed" eliminates unintended future matches without breaking current behaviour.
Minor (no block): get_subnet cache key not lowercased (case-insensitive Azure IDs can miss cache), _indeterminate_finding closure in AZ-CMP-004 is a late-binding maintenance trap, _is_fresh uses age.days floor making the 30-day boundary off by up to 24 h, and get_subnet/get_vm_patch_status re-instantiate SDK clients per call instead of using the shared client pattern used elsewhere.
TFT444
left a comment
There was a problem hiding this comment.
AZ-CMP-003 silent false-negative: break on the first succeeded extension stamps the VM compliant without checking remaining extensions. A VM with IaaSAntimalware (succeeded) + MDE.Linux (failed) passes, directly contradicting the "unhealthy wins" Defender contract in the same rule. Fix: iterate all matched extensions before marking healthy.
AZ-CMP-003 overly broad substring: "edr solution" matches any future Defender recommendation containing those words, risking false-negative coverage gaps. Fix: tighten to "edr solution should be installed".
Summary
Issue #268 claimed three compute rules relied on weak signals that produce false positives or
false negatives. Read the current code first (per the issue's own instruction to verify rather
than assume), confirmed all three claims across two review rounds, and fixed each.
Root cause per rule
scanner/rules/az_cmp_001.py): only checkednic.network_security_group.A VM whose NIC had no NSG of its own but whose subnet carried one -- a common, valid Azure
pattern -- was flagged as a false-positive HIGH finding.
scanner/rules/az_cmp_003.py): only checked whether a recognisedendpoint-protection extension name was present (
KNOWN_EP_EXTENSIONS), with no health check.An installed-but-failed EP extension read as a silent compliant pass -- a false negative.
scanner/rules/az_cmp_004.py): relied only on config flags(
patch_mode/enable_automatic_updates). A VM configured for automatic patching can still bemonths behind if the platform hasn't actually applied anything -- config alone doesn't prove
patches landed, a false negative.
What changed
AzureClient.get_subnet(subnet_id): resolves a NIC's subnet to check for a subnet-level NSG,cached for the client's lifetime.
az_cmp_001.scan(): a VM is non-compliant only when it has a public IP and no NIC-level NSGand no subnet-level NSG. An unresolvable subnet is now treated as indeterminate (LOW,
subnet_nsg_attached: None), never as confirmed unprotected -- an unreadable subnet must notproduce a confirmed HIGH finding.
AzureClient.get_security_assessments(): lists Microsoft Defender for Cloud securityassessments for the subscription (new
azure-mgmt-security==7.0.0dependency), cached for theclient's lifetime.
az_cmp_003.scan(): Defender's "Endpoint protection" assessment is now the primary signal --Defender confirming Healthy/Unhealthy is real agent telemetry and overrides extension-name
presence. Falls back to the extension check (now also validating
provisioning_state, with anon-"Succeeded" state surfaced as indeterminate LOW rather than a silent pass) only when
Defender's signal is unavailable. The per-VM lookup is now built from a single up-front index of
the subscription-wide assessments list instead of rescanning it per VM, and when a resource has
more than one "endpoint protection" assessment, an Unhealthy code always wins regardless of API
response order.
AzureClient.get_vm_patch_status(): fetches a VM's live patch assessment (Azure UpdateManager /
Microsoft.Maintenance) from its runtime instance view.az_cmp_004.scan(): a config-based compliant pass is now overridden into a confirmed findingwhen a conclusive real assessment (
statussucceeded/completedWithWarnings) shows critical orsecurity patches still pending. Config with auto-patching disabled remains a finding
regardless of assessment data, since that's unmanaged-drift risk on its own.
tests/test_rules_compute.py,tests/test_azure_client_management.py, andtests/helpers/mock_azure.pyfor all of the above,including duplicate-extension handling, subnet caching, Defender health precedence, an
order-independence regression test for multiple endpoint-protection assessments per resource,
and an SDK-shape guard for the patch-assessment response.
Tested
python -m pytest tests/test_rules_compute.py tests/test_azure_client_management.py -q--all passing
ruff check/ruff format --check-- all checks passedFixes #268