Repository navigation
Conversation
629f8ab to
c58e3c0
Compare
c58e3c0 to
28968e9
Compare
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files
|
|
@TFT444, please pause this automation PR before further implementation or review. It currently introduces |
parthrohit22
left a comment
There was a problem hiding this comment.
Looked through this — the transaction/fail-closed structure in LifecycleService.apply_scan() is solid (idempotency sentinel inserted last, FOR UPDATE locking, resolution gated on resolving_rule_ids so a scan with no clean outcomes can't silently resolve anything), and the Patterns API's tenant/subscription scoping is done right (JWT is authoritative, query param can only narrow, both columns actually applied in the WHERE clause, not just validated).
Holding off on approve/changes since this is still draft and CI has one red test: test_finding_lifecycle.py::TestLifecycleStateTransitions::test_reopened_finding_absent_from_success_scan_is_resolved (assert False). The resolution SQL itself looks right at a glance (correctly includes REOPENED in the state filter), so this reads more like a mock call-count mismatch in the test than a logic bug, but worth running locally before this comes out of draft. Ping me for another look once it's green and ready.
|
This PR has been inactive for 14 days. Please update or it will be closed in 7 days. |
Add six new tables (Alembic migration e1f2a3b4c5d6), two service classes, a patterns API route, and engine outcome recording. Tables added: - scan_rule_outcomes: per-rule status per scan - scan_lifecycle_applications: idempotency sentinel - finding_fingerprints: stable immutable SHA-256 identity per finding - finding_lifecycles: mutable OPEN/RESOLVED/ACCEPTED/SUPPRESSED/REOPENED state - finding_lifecycle_transitions: append-only audit trail - patterns: published persistent_finding / cross_resource_recurrence / reopened_finding detections Services: - LifecycleService.apply_scan(): idempotent, fail-closed, single transaction with FOR UPDATE row locking - PatternService.detect_and_publish(): three pattern types with hardcoded thresholds stored in the record Routes: - GET /api/v1/patterns (list with subscription_id / pattern_type / limit) - GET /api/v1/patterns/<id> (single pattern or 404) Engine: - ScanEngine.run_scan() now records per-rule outcome status (SUCCESS / EMPTY_SUCCESS / PERMISSION_DENIED / TIMEOUT / FAILED) Tests: - tests/test_finding_lifecycle.py: 18 cases (mocked DB, no live Postgres) - tests/test_patterns.py: 9 cases (service unit + Flask route tests) Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
GET /api/v1/patterns: the subscription scope is now always enforced in SQL (WHERE subscription_id = %s, never a nullable IS NULL bypass). The JWT subscription_id is the authority; a query-param that disagrees with the JWT is rejected with 400. A request with no subscription_id in either the JWT or the query param is also rejected. GET /api/v1/patterns/<id>: the WHERE clause now includes subscription_id so a caller cannot enumerate patterns from other subscriptions by ID. An out-of-scope ID returns 404 to avoid disclosing that the pattern exists in another subscription. Adds three new regression tests: no-subscription-400, cross-subscription query-param-400, and cross-subscription IDOR-404. Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Critical fixes: - Wire LifecycleService.apply_scan() and PatternService.detect_and_publish() into scanner/worker.py immediately after db.save_scan(); lifecycle failures are non-fatal so the scan record is always preserved - Write durable per-rule outcome rows to scan_rule_outcomes at the start of apply_scan so the audit record exists even if lifecycle processing fails - Add subscription_id = effective_sub filter to GET /api/v1/patterns/<id> via _effective_subscription() helper (already present in patterns.py HEAD) Important fixes: - Add UNIQUE(pattern_type, lifecycle_id, scan_id) constraint to patterns table in migration; change ON CONFLICT DO NOTHING to ON CONFLICT ON CONSTRAINT uq_patterns_type_lifecycle_scan DO NOTHING - Collect detection query results before closing RealDictCursor, then open fresh cursors for each _upsert_pattern call, eliminating nested-cursor overlap in pattern_service.py - Narrow the absent-findings bulk lock to only rules with a resolving outcome (resolving_rule_ids); skip the query entirely when no rule produced SUCCESS/EMPTY_SUCCESS, reducing unnecessary lock contention - Reset consecutive_success_count = 0 when a RESOLVED/ACCEPTED/SUPPRESSED finding is reopened - Remove unused _BLOCKING_STATUSES frozenset from lifecycle_service.py - Move 'import json' to module level in pattern_service.py Test fixes: - Refactor _FakeCursor/_FakeConn to use a shared deque so multiple cursor() calls on one connection consume from the same result stream - Update scripted result sequences in all tests to include the new scan_rule_outcomes INSERT in the execute order - Update fail-closed tests to reflect that no absent-findings query is issued when resolving_rule_ids is empty (FAILED/PERMISSION_DENIED) - Add test_scan_rule_outcomes_written to verify audit record is emitted - Add consecutive_success_count = 0 assertion to reopen test Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
…ponses Five routes were echoing str(exc) directly to the client in error response bodies, exposing DB connection strings, filesystem paths, and stack trace fragments. The JWT middleware was also leaking the specific JWT validation failure reason via f-string interpolation. Changes: - api/app.py: InvalidTokenError now returns generic 'Invalid token' (no exc detail) - api/routes/compliance.py: FileNotFoundError logs path, returns opaque message; general handler drops 'detail' field - api/routes/findings.py: both endpoints drop 'detail: str(exc)' from 500 body - api/routes/scans.py: all four catch blocks drop 'detail: str(exc)' from 500 body Internal errors are still logged server-side at ERROR level. Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Critical: - lifecycle_service: wrap entire transaction in try/except with explicit rollback() to prevent connection wedge on mid-transaction failure - pattern_service: same rollback guard for detect_and_publish transaction - lifecycle_service: replace NOT IN %s + tuple() with != ALL(%s) + list() to fix single-element tuple syntax error (PostgreSQL rejects trailing comma) - patterns API: add tenant_id = %s to both SQL WHERE clauses to close cross-tenant pattern read in multi-tenant deployments Important: - patterns API: add _effective_tenant() helper using OPENSHIELD_TENANT_ID env - migration: add 4 missing indexes (fingerprints tenant+sub, lifecycles state partial, transitions lifecycle_id, patterns sub+created_at) - lifecycle_service: remove unused psycopg2.extras import - test_finding_lifecycle: add test_rule_a_success_does_not_resolve_rule_b_finding to verify cross-rule resolution isolation (the key correctness invariant) - test_finding_lifecycle: add rollback() to _FakeConn and _TrackingConn - test_patterns: assert subscription_id appears in SQL params for IDOR test and subscription scoping test (not just mock return value) Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
- Add _VALID_OUTCOME_STATUSES allowlist; unknown status defaults to FAILED instead of letting DB constraint throw IntegrityError mid-transaction - Fix dead-code if/elif block: restructure to if resolving_rule_ids / nested if seen_fingerprint_ids (fail-closed logic now clear at a glance) - Add tenant_id to ix_patterns_sub_created index (was subscription_id only; multi-tenant queries filter on both columns) - Add CASCADE to all downgrade() DROP TABLE statements (future FK safety) - Add row_version comment clarifying it is a monotonic counter, not OCC - Fix _FakeConn in test_patterns.py to share a single deque across all cursor() calls, matching test_finding_lifecycle.py and real psycopg2 behaviour - Add REOPENED -> RESOLVED test covering finding absent from SUCCESS scan Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
26361c2 to
76b6030
Compare
|
Heads-up @TFT444: #325 merged into The migration also needs rebasing. Before your next review request, could you rebase onto current |
…cope Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
SHAURYAKSHARMA24
left a comment
There was a problem hiding this comment.
ritiksah141's four items cover the durability, tenant casing and workflow changes, so I won't repeat those. Two more things came up when I ran this together with #381, plus one case to add to item 1.
Doesn't work once #381 lands. After #381, a rule with evaluate() goes through _run_evaluate() and hits continue before the scan() branch, so it never gets a rule_outcomes entry. apply_scan() only resolves when outcome_by_rule[rule] is in _RESOLVING_STATUSES, so for a migrated rule a fixed resource stays OPEN for good. This PR's own test shows it: merge #382 (which contains #381) into this branch and test_engine_retains_legacy_outcomes_and_resource_evaluations fails with an IndexError on rule_outcomes[0]. I also ran it end to end on Postgres with KV-006: violating scan -> OPEN, then a scan where the vault is fixed (PASS) -> still OPEN, with no outcome row. On this branch alone the same sequence resolves correctly. Since #380 moves every rule onto evaluate(), resolution would gradually stop working everywhere.
That and the EMPTY_SUCCESS comment inline point the same way as m-khan-97's note from August. I'd drop rule_outcomes and resolve from what the engine already produces: a (rule, resource) resolves when its evaluation in this scan is PASS and the rule isn't in failed_rule_ids, and ERROR/UNKNOWN leaves it alone. That behaves the same before and after #381, and it avoids a second outcome signal that can disagree with the evaluations.
Apply order (adds to item 1). Nothing in apply_scan() checks whether the scan being applied is older than one already applied. On Postgres: A (violating) applied -> OPEN, B (violating) saved but not applied yet, C (fixed) applied -> RESOLVED, then B applied -> REOPENED, even though C is the newest evidence. With the current inline call that needs two overlapping scans, but a retry queue makes it a normal path, so the lifecycle job should no-op when a newer scan for the subscription has already been applied.
Happy to share the scripts I used for these if useful.
| finding.setdefault("scan_id", scan_id) | ||
| validated_findings.append(finding) | ||
| findings.extend(validated_findings) | ||
| outcome_status = "SUCCESS" if validated_findings else "EMPTY_SUCCESS" |
There was a problem hiding this comment.
For legacy rules this records EMPTY_SUCCESS when the scan couldn't see anything. The collectors catch the exception and return [] (e.g. get_storage_accounts, get_network_security_groups), so scan() returns normally and the 403 branch below never fires. I patched the SDK clients to raise a 403, a 429 and a ClientAuthenticationError; every rule wrote EMPTY_SUCCESS to scan_rule_outcomes. It doesn't resolve anything wrongly on its own because a PASS is also required, but it's a stored row saying the rule succeeded, and patterns/reporting will read it.
| """, | ||
| (scan_id, lifecycle_id), | ||
| ) | ||
| elif state in ("RESOLVED", "ACCEPTED", "SUPPRESSED"): |
There was a problem hiding this comment.
This is where the apply-order case bites: a late apply of an older scan moves RESOLVED back to REOPENED. It would do the same to ACCEPTED/SUPPRESSED, which are user decisions, so a stale apply could undo a suppression.
|
|
||
| def _effective_subscription(subscription_id_param: str | None) -> str: | ||
| """The trusted token issuer must assign an explicit subscription scope.""" | ||
| subscription = (getattr(g, "user", {}) or {}).get("subscription_id") |
There was a problem hiding this comment.
Adding to ritiksah141's tenant point: Entra access tokens don't carry a subscription_id claim, so in OIDC mode this raises for every real user and the patterns endpoints always return 400, even with tenant casing fixed. The allowed subscriptions probably need to come from server-side config or a principal-to-subscription mapping rather than the token.
- worker.py: add exc_info=True to lifecycle error log so stack traces appear in production; remove unused lc_exc variable name - lifecycle_service.py: assert autocommit=False at apply_scan entry to catch misconfigured connections before any writes happen - patterns.py: remove unused effective_sub parameter from _effective_tenant - .env.example: document OPENSHIELD_TENANT_ID Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
|
Hi @parthrohit22 @ritiksah141 @Vishnu2707 @SHAURYAKSHARMA24 @vogonPrayas - all review findings have been addressed and pushed (76a656c):
All 91 tests pass locally. CI is fully green. Ready for re-review, would appreciate an approve if this looks good. |
…ntry is present Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
parthrohit22
left a comment
There was a problem hiding this comment.
Re-reviewed ef8f6e7. The autocommit guard and the dead-parameter cleanup are good. But none of @ritiksah141's four items from 9 Oct are addressed yet, and the new evaluate()-only path has a gap of its own.
1. Still open from the 9 Oct reviews
- Durability (Ritik 1): lifecycle and pattern application is still a
try/exceptaftersave_scancommits. A crash in that window still loses the application permanently. - Tenant casing (Ritik 2):
scanner/worker.py:226still writesOPENSHIELD_TENANT_IDunnormalised, falling back to the subscription ID, and_effective_tenant()still compares case-sensitively. In a default deployment the patterns endpoint still returns nothing. - CI edits (Ritik 3 and 4): the
ports['5432']change, thefeat/*triggers in five workflows andtests/test_feature_stack_ci.pyare all still in the diff. - Apply order (@SHAURYAKSHARMA24): nothing stops an older scan from being applied after a newer one, so the B-after-C reopen still happens. Once item 1 adds a retry queue, this becomes a normal path rather than a race.
2. evaluated_cleanly ignores failed_rule_ids
e33ff03 treats an evaluate()-only rule as having completed cleanly if any of its evaluations isn't ERROR. With #369, the engine keeps the valid items when evaluate() returns a partly malformed list, adds an ERROR evaluation and puts the rule in failed_rule_ids. Under this check that rule still counts as clean, so its PASS resources resolve, while compliance scoring treats the same rule as ERROR for the same scan. The two outputs disagree.
Shaurya's rule avoids this and doesn't need rule_outcomes at all: a (rule, resource) resolves when its evaluation in this scan is PASS and the rule is not in the scan's failed_rule_ids; ERROR/UNKNOWN leave it alone. Please pass failed_rule_ids into apply_scan() and gate on it, with a test where a rule has one PASS and one ERROR evaluation and the PASS resource is not resolved.
Merge order
#352, #353 and #354 now chain onto this PR's a8d9c2e4f601, and #384 chains off the same parent (b6d2f8a4c1e7). Whichever lands after this needs to repoint. The ritik non-blocking list (stale PR body, finding_ids naming, completed_at being dropped, deleted-resource resolution, patterns retention) still applies.
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
…nd fencing Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
|
All 4 blockers have been addressed. Changes pushed to this branch:
Ready for re-review @ritiksah141 @parthrohit22 @Vishnu2707 @SHAURYAKSHARMA24 |
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
Added a scan ordering guard that checks whether a newer scan has already been applied for the same (subscription, tenant) pair before processing the current scan. If a newer scan exists in scan_lifecycle_applications, the older scan is silently skipped to prevent stale retries from rewinding lifecycle state. Added a unit test (test_older_scan_does_not_overwrite_newer_scan_state) that verifies the ordering guard returns without committing when the fake DB returns a newer scan row. Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
…reads Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
The feat/* branch entries in all five workflow files and tests/test_feature_stack_ci.py were temporary scaffolding to run CI on the stacked PRs during development. They must not land in dev. Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
parthrohit22
left a comment
There was a problem hiding this comment.
Re-reviewed ecf6d71. Thanks, this is a big step forward. I checked the earlier blockers and they’re resolved:
Lifecycle jobs are queued in the same transaction as save_scan, with a fenced lease and limited retries.
There’s an advisory lock, and an older scan can no longer be applied after a newer one.
A rule in failed_rule_ids can no longer resolve findings.
Tenant IDs are lowercased on write and read.
The CI scaffolding is gone.
One blocker remains:
scanner/lifecycle_worker.py needs OPENSHIELD_TENANT_ID or AZURE_TENANT_ID. With managed identity often neither is set. Then every job fails three times, ends as failed, and the feature switches itself off with only a log line.
Please check this when the worker starts (fail fast or emit a clear warning/metric). Also document it as required in .env.example, which currently leaves it blank.
Also, please drop the requirements* changes, since OWASP/openshield#401 covers them. Once both are done I’m happy to approve. @ritiksah141 @SHAURYAKSHARMA24, a final pass from you on the lifecycle job path would be good.
Dependency lock updates are handled in #401 (fix/security-deps-python). Removing from this PR to keep the diff focused on finding lifecycle. Signed-off-by: Tanvir Farhad <tanvirfarhad007@gmail.com> Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
|
@parthrohit22 @ritiksah141 Requirements changes have been dropped since #401 covers them, and the OPENSHIELD_TENANT_ID startup check is already in place in |
Add a module-level check so misconfiguration is visible immediately in the worker log on boot, rather than silently failing inside each job three times before it is marked failed. Signed-off-by: Tanvir Farhad <tanvirfarhad007@gmail.com> Signed-off-by: Tanvir Farhad <tamimtarafder12@gmail.com>
|
@parthrohit22 Startup warning added in |
|
@parthrohit22 last item addressed: |
Summary
Saving a scan now enqueues lifecycle work in the same transaction. Lifecycle transitions, pattern publication and job completion commit atomically under the job fence and tenant/subscription lock. Expired workers cannot write; delayed scans cannot rewind newer evidence or publish patterns under an older scan ID. Resolution uses resource PASS evidence with failed-rule safeguards. Internal reads process findings beyond the public 1,000-row cap, and cross-resource patterns count distinct resources.
Runtime dependency inputs and Linux Python 3.11 hash locks include the patched aiohttp, multidict, Werkzeug and certifi versions. No new vulnerability suppressions were added.
Validation
87e870d0eb30df83323ec8b65c5cfe9aa36aa0e0.Pattern reads accept verified signed scope or a matching tenant deployment's authorized subscription configuration. Missing tenant configuration fails closed. PostgreSQL regressions cover rollback, retries, stale ownership, ordering, 1,001 findings, resource fingerprint variants and subscription selection. Explicit read-only workflow triggers cover this named feature stack; deployment triggers are unchanged.
Review status
Code and current-head CI are ready for independent re-review. Existing reviewer approvals and conversations are intentionally left for reviewers to confirm; this is not approval or merge authorization.