fix(scanner): remove compute-rule false positives/negatives (#268) - #272
fix(scanner): remove compute-rule false positives/negatives (#268)#272parthrohit22 wants to merge 4 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. |
c36673f to
47403e5
Compare
|
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".
…rker - Stop breaking out of the extension loop on the first Succeeded record. A VM with one healthy and one genuinely failed recognised extension (e.g. IaaSAntimalware succeeded, MDE.Linux failed) was silently stamped compliant, with the failed extension never appearing in finding metadata. Records are now grouped by extension name so duplicate reports of the same extension still resolve on 'any succeeded', while distinct extensions follow the same unconfirmed-wins precedent already used for Defender assessments above. - Tighten the EDR display-name marker from the bare substring 'edr solution' to the full recommendation title 'edr solution should be installed', so an unrelated future recommendation containing those two words can't be mistaken for this rule's Defender signal. Addresses TFT444's review on OWASP#272. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
|
@TFT444 both fixed:
Added regression tests for both. Re-requesting your review. |
SHAURYAKSHARMA24
left a comment
There was a problem hiding this comment.
Most of the earlier blockers are fixed: the subnet/multi-NIC paths, Defender precedence and extension ordering all behave correctly, and the current head merges cleanly with dev with the focused and full backend suites passing.
One AZ-CMP-004 false-positive path remains. Freshness is only checked after a zero patch count, so a conclusive summary with pending patches becomes HIGH before last_modified_time is validated. I reproduced both a 45-day-old summary and one with no timestamp returning patch_assessment_override/HIGH; either can describe patches already remediated and should be indeterminate, not a confirmed current violation. Please require a usable, current timestamp before trusting nonzero counts too, and cover the stale/missing-timestamp pending cases (the freshness helper should also reject materially future timestamps).
Non-blocking integration cleanup: merging current dev leaves azure-mgmt-security==7.0.0 duplicated in requirements.txt.
…rker - Stop breaking out of the extension loop on the first Succeeded record. A VM with one healthy and one genuinely failed recognised extension (e.g. IaaSAntimalware succeeded, MDE.Linux failed) was silently stamped compliant, with the failed extension never appearing in finding metadata. Records are now grouped by extension name so duplicate reports of the same extension still resolve on 'any succeeded', while distinct extensions follow the same unconfirmed-wins precedent already used for Defender assessments above. - Tighten the EDR display-name marker from the bare substring 'edr solution' to the full recommendation title 'edr solution should be installed', so an unrelated future recommendation containing those two words can't be mistaken for this rule's Defender signal. Addresses TFT444's review on OWASP#272. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
792ef98 to
92af031
Compare
|
Rebased onto current ####@SHAURYAKSHARMA24 - AZ-CMP-004 freshness-ordering false positive (blocker) Fixed in
So a 45-day-old summary or one with no
New regression tests in
Non-blocking cleanup
@TFT444 — AZ-CMP-003 (from the 07:35 review)Both points were addressed in Verification
@SHAURYAKSHARMA24 @TFT444 — re-review please. |
TFT444
left a comment
There was a problem hiding this comment.
Re-review of current head (92af031) against SHAURYAKSHARMA24's active blocker (2026-09-02).
The one active blocker is resolved.
SHAURYAKSHARMA24 reported: "Freshness is only checked after a zero patch count, so a conclusive summary with pending patches becomes HIGH before last_modified_time is validated."
This does not match the code at the current head. The _is_fresh gate fires unconditionally after critical_count is extracted and before the critical_count > 0 branch:
if not _is_fresh(getattr(patch_summary, "last_modified_time", None)):
findings.append(_indeterminate_finding("assessment_stale", patch_summary))
continue
if critical_count > 0:
findings.append(...) # HIGH overrideA 45-day-old summary with 3 pending patches hits assessment_stale and returns LOW. A summary with no last_modified_time also hits assessment_stale because _is_fresh(None) returns False immediately. Both scenarios have dedicated regression tests: test_cmp_004_config_ok_but_stale_pending_patch_assessment_is_indeterminate_not_high and test_cmp_004_config_ok_but_pending_patch_assessment_with_no_timestamp_is_indeterminate_not_high. Future timestamps rejected by _is_fresh are also addressed, with a tolerance test for benign clock skew.
The azure-mgmt-security==7.0.0 duplication SHAURYAKSHARMA24 flagged in requirements.txt is not present on the current head; exactly one entry exists.
TFT444's two prior blockers are also still resolved.
-
AZ-CMP-003
breakon first succeeded extension (multi-extension false-negative): Fixed. The new code groups by extension name intostates_by_name, then derivesunconfirmed_namesby checking whether"succeeded"appears in each name's state list. A VM with IaaSAntimalware (Succeeded) and MDE.Linux (Failed) correctly lands inunconfirmed_names.test_cmp_003_one_succeeded_and_one_failed_extension_is_indeterminate_not_a_passcovers both NIC orderings. Thebreakis gone. -
AZ-CMP-003 overly broad
"edr solution"substring: Fixed._ENDPOINT_PROTECTION_DISPLAY_NAME_MARKERSnow contains the full string"edr solution should be installed".test_cmp_003_edr_solution_substring_alone_does_not_match_unrelated_recommendationverifies that"Review edr solution licensing costs"falls through to the extension check rather than being picked up as a Defender signal.
Non-blocking observations (no action required):
_is_freshusestimedelta.dayswhich floors to integer days, so the effective freshness window is up to ~24 hours wider than the stated 30-day threshold. Noted in the prior round; acceptable given the threshold is already a conservative buffer.get_vm_patch_statusre-instantiates the SDK client per call with no cache. Consistent with the pattern used elsewhere for infrequent non-list fetches; does not block merge._indeterminate_findingis a closure insidescaninaz_cmp_004, slightly inconsistent with codebase style. No correctness impact.
Approving.
fdcc524
92af031 to
fdcc524
Compare
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
fdcc524 to
f72758b
Compare
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>
|
Pushed 8fad850 to get CI green. Root cause: the dev rebase left a duplicated copy of the AZ-CMP-007 helper block + tests sitting before the AZ-CMP-004 assessment tests. That stale copy defined a local Effect:
Fix: removed the 206-line duplicated block. The canonical AZ-CMP-007 block at end of file is kept (it uses the renamed Local: |
`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>
TFT444
left a comment
There was a problem hiding this comment.
Re-reviewed the latest head, including the compute-rule false positive/negative fixes and the style commit.
The indeterminate/confirmed split across AZ-CMP-001, 003, and 004 is consistent and correct. The confirmed/indeterminate upgrade logic in AZ-CMP-001 (an indeterminate NIC finding can be replaced by a later confirmed HIGH, but an existing indeterminate cannot be overwritten by another indeterminate) is right. The _subnet_nsg_status three-value return avoids conflating 'unresolvable' with 'no NSG', which was the root of the false positive. AZ-CMP-003 correctly builds the Defender assessment index once per scan at O(n) and then does O(1) per-VM lookups, and the 'unhealthy wins across all matching assessments' logic handles iteration-order sensitivity. The _is_fresh helper in AZ-CMP-004 is conservative (missing timezone, future timestamps, and unparseable strings all return False) and the stale-assessment indeterminate result prevents a stale clean assessment from masking newly available patches.
The mock client updates and the set_subnet/get_security_assessments/get_vm_patch_status additions match the production interface. No issues found.
Approving.
|
@SHAURYAKSHARMA24 please have a look, it looks clean and mergeable now. |
|
@SHAURYAKSHARMA24 rebased onto current The AZ-CMP-004 freshness-ordering blocker from your Sep 2 review has been fixed since Sep 2: @TFT444 re-verified this exact path and approved on Sep 6. Re-requesting your review since it's the only outstanding change request. |
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