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
9 changes: 7 additions & 2 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5161,8 +5161,9 @@ def quality_check(self) -> dict:

def compute_requirement_assessments_results(
self, mapping_set: RequirementMappingSet, source_assessment: Self
) -> list["RequirementAssessment"]:
) -> tuple[list["RequirementAssessment"], dict["RequirementAssessment", list[str]]]:
requirement_assessments: list[RequirementAssessment] = []
assessment_source_dict: dict[RequirementAssessment, list[str]] = {}
result_order = (
RequirementAssessment.Result.NOT_ASSESSED,
RequirementAssessment.Result.NOT_APPLICABLE,
Expand Down Expand Up @@ -5227,6 +5228,10 @@ def assign_attributes(target, attributes):
)
ref = refs[inferences.index(selected_inference)]

assessment_source_dict[requirement_assessment] = [
str(ref.id) for ref in refs
]

assign_attributes(requirement_assessment, selected_inference)
requirement_assessment.mapping_inference = {
"result": requirement_assessment.result,
Expand All @@ -5253,7 +5258,7 @@ def assign_attributes(target, attributes):
],
batch_size=1000,
)
return requirement_assessments
return requirement_assessments, assessment_source_dict

def get_progress(self) -> int:
requirement_assessments = list(
Expand Down
34 changes: 23 additions & 11 deletions backend/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import regex
import os
import uuid
import itertools
import zipfile
import tempfile
from datetime import date, datetime, timedelta
Expand Down Expand Up @@ -5943,15 +5944,16 @@ def perform_create(self, serializer):
)

# Compute results and get all affected requirement assessments
computed_assessments = instance.compute_requirement_assessments_results(
mapping_set, baseline
computed_assessments, assessment_source_dict = (
instance.compute_requirement_assessments_results(
mapping_set, baseline
)
)

# Collect all source requirement assessment IDs
source_assessment_ids = [
assessment.mapping_inference["source_requirement_assessment"]["id"]
for assessment in computed_assessments
]
source_assessment_ids = itertools.chain.from_iterable(
assessment_source_dict.values()
)

# Fetch all baseline requirement assessments in one query
baseline_assessments = {
Expand All @@ -5966,21 +5968,31 @@ def perform_create(self, serializer):
m2m_operations = []

for requirement_assessment in computed_assessments:
source_id = requirement_assessment.mapping_inference[
selected_source_id = requirement_assessment.mapping_inference[
"source_requirement_assessment"
]["id"]
baseline_ra = baseline_assessments[source_id]
selected_baseline_ra = baseline_assessments[selected_source_id]

# Update observation
requirement_assessment.observation = baseline_ra.observation
requirement_assessment.observation = (
selected_baseline_ra.observation
)
updates.append(requirement_assessment)

source_ids = assessment_source_dict[requirement_assessment]
baseline_requirement_assessments = [
baseline_assessments[source_id] for source_id in source_ids
]

# Store M2M operations for later
m2m_operations.append(
(
requirement_assessment,
baseline_ra.evidences.all(),
baseline_ra.applied_controls.all(),
selected_baseline_ra.evidences.all(),
itertools.chain.from_iterable(
requirement_assessment.applied_controls.all()
for requirement_assessment in baseline_requirement_assessments
),
)
)
Comment on lines 5989 to 5997
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix generator capturing the wrong applied controls

itertools.chain.from_iterable(...) captures baseline_requirement_assessments by reference. Because you append that generator to m2m_operations and only iterate it after the outer loop finishes, every stored generator runs with baseline_requirement_assessments set to whatever the last iteration assigned. Earlier requirement assessments therefore end up copying the applied controls of the final baseline mapping, which is a regression. Materialize the union while you’re still inside the loop (e.g. build a list or set of control IDs) and store that concrete collection instead of the lazy generator.

Suggested fix:

-                    m2m_operations.append(
-                        (
-                            requirement_assessment,
-                            selected_baseline_ra.evidences.all(),
-                            itertools.chain.from_iterable(
-                                requirement_assessment.applied_controls.all()
-                                for requirement_assessment in baseline_requirement_assessments
-                            ),
-                        )
-                    )
+                    baseline_control_ids = {
+                        ac.id
+                        for baseline_ra in baseline_requirement_assessments
+                        for ac in baseline_ra.applied_controls.all()
+                    }
+                    m2m_operations.append(
+                        (
+                            requirement_assessment,
+                            selected_baseline_ra.evidences.all(),
+                            baseline_control_ids,
+                        )
+                    )
@@
-                for assessment, evidences, controls in m2m_operations:
-                    assessment.evidences.add(*[ev.id for ev in evidences])
-                    assessment.applied_controls.add(*[ac.id for ac in controls])
+                for assessment, evidences, control_ids in m2m_operations:
+                    assessment.evidences.add(*[ev.id for ev in evidences])
+                    if control_ids:
+                        assessment.applied_controls.add(*control_ids)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(
requirement_assessment,
baseline_ra.evidences.all(),
baseline_ra.applied_controls.all(),
selected_baseline_ra.evidences.all(),
itertools.chain.from_iterable(
requirement_assessment.applied_controls.all()
for requirement_assessment in baseline_requirement_assessments
),
)
)
baseline_control_ids = {
ac.id
for baseline_ra in baseline_requirement_assessments
for ac in baseline_ra.applied_controls.all()
}
m2m_operations.append(
(
requirement_assessment,
selected_baseline_ra.evidences.all(),
baseline_control_ids,
)
)
🤖 Prompt for AI Agents
In backend/core/views.py around lines 5989 to 5997 the code appends an
itertools.chain.from_iterable(...) generator that closes over
baseline_requirement_assessments, causing all stored generators to reflect the
final loop value; instead, inside the loop materialize the applied-controls
collection (e.g., build a list or set of control IDs or control instances from
requirement_assessment.applied_controls.all() or from the baseline mapping) and
append that concrete collection to m2m_operations so later iteration uses the
correct, per-iteration controls rather than a late-bound generator.


Expand Down
Loading