Skip to content

Conversation

@ab-smith
Copy link
Contributor

@ab-smith ab-smith commented Nov 2, 2025

Summary by CodeRabbit

  • New Features

    • End-to-end conversion: convert qualitative assessments into quantitative studies, runs Monte Carlo simulations, copies authors/reviewers, and returns conversion summary (new study id, converted/skipped counts).
    • Conversion workflow includes structured validation for probability/impact anchors and loss threshold.
  • UI

    • New conversion page and action button with validation, progress overlay, success/error toasts, and optional loss-threshold.
    • List view now shows updated timestamp column.
  • Localization

    • Extensive new English and French UI text for the quantitative conversion workflow and validation messages.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 2, 2025

Walkthrough

Adds a backend REST action to convert qualitative RiskAssessments into QuantitativeRiskStudies, new frontend UI/route/form to collect anchoring and submit conversion, updates list table columns, and expands English/French i18n for the quantitative conversion workflow.

Changes

Cohort / File(s) Summary
Backend — Conversion Endpoint
backend/core/views.py
Added convert_to_quantitative REST action on RiskAssessmentViewSet to validate payload and permissions, create a QuantitativeRiskStudy, iterate and convert scenarios to QuantitativeRiskScenario and hypotheses, copy authors/reviewers, run simulations, skip invalid scenarios, and return conversion summary (study id, converted/skipped counts).
Frontend — Conversion UI & Routes
frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/+page.svelte, frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/convert-to-quantitative/+page.server.ts, frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/convert-to-quantitative/+page.svelte
Added action button linking to a new conversion page; new server action posts JSON to backend /convert_to_quantitative/, handles errors/success; new conversion page with probability/impact anchoring inputs, validation (bounds, monotonicity, loss threshold), payload construction, Monte Carlo progress overlay, and redirect on success.
Frontend — Form component update
frontend/src/lib/components/Forms/ModelForm/QuantitativeRiskHypothesisForm.svelte
Replaced direct 0–1 probability input with a percentage (0–100) UI backed by a hidden 0–1 probability field; initializes and synchronizes percentage ↔ probability.
Frontend — Table configuration
frontend/src/lib/utils/table.ts
Updated "quantitative-risk-studies" list view: added updatedAt to head and updated_at to body (replacing prior column).
Localization — English
frontend/messages/en.json
Added numerous i18n keys for the convert-to-quantitative workflow: labels, descriptions, validation messages, status/progress and simulation texts, anchors/bounds UI strings.
Localization — French
frontend/messages/fr.json
Added corresponding French translations for the convert-to-quantitative workflow, validation prompts, and status/progress strings.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant FE as Frontend UI
    participant SR as SvelteKit Server Action
    participant API as Backend API
    participant DB as Database

    User->>FE: Click "Convert to Quantitative"
    FE->>FE: Render anchor form (probability & impact, loss threshold)
    User->>FE: Submit form
    FE->>SR: POST form (JSON payload)
    activate SR
    SR->>API: POST /risk-assessments/{id}/convert_to_quantitative/
    activate API
    API->>DB: Create QuantitativeRiskStudy
    loop per scenario
        API->>DB: Create QuantitativeRiskScenario & hypotheses
        API->>API: Run simulations (current/residual) if valid
    end
    API-->>SR: {quantitative_risk_study_id, scenarios_converted, scenarios_skipped}
    deactivate API
    SR-->>FE: JSON response (success or error)
    deactivate SR
    FE->>FE: Show toasts/progress, redirect to created quantitative study on success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay special attention to: backend/core/views.py scenario iteration, validation, simulation invocation and error handling; preservation/copying of authors/reviewers and applied controls linkage; backend transactionality and skip/count semantics.
  • Frontend: validation correctness in +page.svelte, payload formatting in +page.server.ts, and percent↔probability sync in QuantitativeRiskHypothesisForm.svelte.

Suggested labels

High Value

Suggested reviewers

  • eric-intuitem
  • nas-tabchiche
  • Mohamed-Hacene

Poem

🐰 I hopped from qual to quant with glee,

Anchors set and probabilities free,
Monte Carlo sings beneath my paws,
New studies bloom from careful laws,
I nibble numbers, safe with claws.

Pre-merge checks and finishing touches

✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(exp): convert qualitative risk assessments to quantitative ones' directly and clearly describes the main feature being added—a conversion capability from qualitative to quantitative risk assessments, which aligns with the core functionality implemented across backend and frontend changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch quali_to_quanti_converter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ab-smith ab-smith marked this pull request as ready for review November 4, 2025 15:31
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (3)
backend/core/views.py (3)

1982-1992: Avoid orphan studies when nothing is convertible.

If no scenario has valid current/residual values, an empty QuantitativeRiskStudy is still created. Pre-scan scenarios and bail out with 400 when none qualify.

Minimal pattern:

eligible = []
for sc in risk_assessment.risk_scenarios.all():
    if ((sc.current_proba in probability_map and sc.current_impact in impact_map) or
        (sc.residual_proba in probability_map and sc.residual_impact in impact_map)) and \
       (sc.threats.exists() and sc.assets.exists()):
        eligible.append(sc)
if not eligible:
    return Response({"detail": "No scenarios eligible for conversion"}, status=400)
# then create the study and iterate eligible only

Also applies to: 2001-2046


2001-2046: Make scenario conversion atomic to prevent partial writes per scenario.

Wrap each scenario conversion in a transaction to keep objects consistent if any step fails (relations, simulation, etc.).

Apply this diff:

-        for scenario in risk_assessment.risk_scenarios.all():
+        for scenario in risk_assessment.risk_scenarios.all():
+            with transaction.atomic():
                 # existing body unchanged…

Also applies to: 2060-2139


2064-2070: Prefer default hypothesis ref_ids to avoid collisions.

Using scenario.ref_id for hypothesis ref_id can collide; let the model generate default via get_default_ref_id or omit ref_id.

Example:

-                    ref_id=scenario.ref_id or "",
+                    # let default ref_id be generated

Also applies to: 2103-2111

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1ebc01d and 24f5af6.

📒 Files selected for processing (7)
  • backend/core/views.py (1 hunks)
  • frontend/messages/en.json (1 hunks)
  • frontend/messages/fr.json (1 hunks)
  • frontend/src/lib/utils/table.ts (1 hunks)
  • frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/+page.svelte (1 hunks)
  • frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/convert-to-quantitative/+page.server.ts (1 hunks)
  • frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/convert-to-quantitative/+page.svelte (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/convert-to-quantitative/+page.server.ts (1)
frontend/src/lib/utils/constants.ts (1)
  • BASE_API_URL (4-8)
backend/core/views.py (3)
backend/crq/models.py (4)
  • QuantitativeRiskStudy (23-360)
  • QuantitativeRiskScenario (363-524)
  • QuantitativeRiskHypothesis (527-940)
  • run_simulation (595-698)
backend/core/models.py (19)
  • RiskAssessment (4279-4683)
  • create (4123-4125)
  • risk_scenarios (3844-3845)
  • assets (6701-6702)
  • save (1636-1644)
  • save (1760-1763)
  • save (2733-2735)
  • save (3294-3296)
  • save (3369-3376)
  • save (3551-3555)
  • save (3765-3789)
  • save (4136-4138)
  • save (4273-4276)
  • save (4344-4346)
  • save (5007-5033)
  • save (5156-5162)
  • save (6100-6120)
  • save (6649-6662)
  • applied_controls (6705-6706)
backend/iam/models.py (3)
  • RoleAssignment (807-1094)
  • is_access_allowed (837-857)
  • save (349-356)
🔇 Additional comments (3)
frontend/src/lib/utils/table.ts (1)

1919-1920: LGTM: Timestamp column addition

The addition of updatedAt/updated_at columns to the quantitative-risk-studies table configuration is appropriate and follows the established naming conventions (camelCase for head, snake_case for body).

frontend/messages/en.json (1)

2577-2605: No issues found. Localization is correct and matches backend validation.

The backend validation in backend/crq/utils.py confirms that probabilities must satisfy 0 < p < 1 (exclusive), which aligns perfectly with the localization message at line 2598: "Probability for '{level}' must be between 0 and 1 (exclusive). Please use a value like 0.05 or 0.9." The examples provided (0.05, 0.9) are valid under this constraint.

backend/core/views.py (1)

2043-2045: The folder field is confirmed to exist on both models.

Both QuantitativeRiskScenario and QuantitativeRiskHypothesis inherit from FolderMixin, which explicitly defines a folder field as a ForeignKey to Folder. The code at all three affected locations (lines 2043–2045, 2069–2070, and 2109–2110) is correct and will not error.

Likely an incorrect or invalid review comment.

Comment on lines +2003 to +2008
if not scenario.threats.exists() and not scenario.assets.exists():
logger.info(
f"Skipping scenario '{scenario.name}' - no threats or assets associated"
)
scenarios_skipped += 1
continue
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

Bug: skip condition mismatches comment/intent.

Comment says “without threats or assets”; using “and” only skips when both are missing. Use “or” to skip when either side is missing.

Apply this diff:

-            # Skip scenarios without threats or assets
-            if not scenario.threats.exists() and not scenario.assets.exists():
+            # Skip scenarios without threats or assets
+            if not scenario.threats.exists() or not scenario.assets.exists():
📝 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
if not scenario.threats.exists() and not scenario.assets.exists():
logger.info(
f"Skipping scenario '{scenario.name}' - no threats or assets associated"
)
scenarios_skipped += 1
continue
if not scenario.threats.exists() or not scenario.assets.exists():
logger.info(
f"Skipping scenario '{scenario.name}' - no threats or assets associated"
)
scenarios_skipped += 1
continue
🤖 Prompt for AI Agents
In backend/core/views.py around lines 2003 to 2008 the skip condition currently
uses "and" so it only skips when both threats and assets are missing, which
contradicts the comment/intent; change the condition to use "or" so scenarios
missing either threats or assets are skipped, and update the log message if
needed to reflect "no threats or no assets" (ensure scenarios_skipped increment
and continue remain unchanged).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/convert-to-quantitative/+page.svelte (1)

339-379: Attach payload inside enhance so all submission paths work (Enter/AT included).

Currently the hidden input is only populated in the button’s onclick handler. Submitting via Enter or assistive tech skips it and posts an empty payload.

Move serialization into enhance, set formData there, and drop the onclick.

-			<form
-				method="POST"
-				use:enhance={() => {
-					const payload = validateAndPrepareData();
-					if (!payload) {
-						return async ({ update }) => {
-							await update({ reset: false });
-						};
-					}
-
-					isSubmitting = true;
-
-					return async ({ result, update }) => {
-						await update();
-						if (result.type !== 'success') {
-							isSubmitting = false;
-						}
-					};
-				}}
-			>
-				<input type="hidden" name="data" value="" />
+			<form
+				method="POST"
+				use:enhance={(form) => {
+					return async ({ formData, cancel, update }) => {
+						const payload = validateAndPrepareData();
+						if (!payload) {
+							cancel();
+							await update({ reset: false });
+							return;
+						}
+						formData.set('data', JSON.stringify(payload));
+						isSubmitting = true;
+						const result = await update();
+						if (!result || result.type !== 'success') {
+							isSubmitting = false;
+						}
+					};
+				}}
+			>
+				<input type="hidden" name="data" />
@@
-					<button
+					<button
 						type="submit"
 						class="btn preset-filled-primary-500"
 						disabled={isSubmitting}
-						onclick={(e) => {
-							const payload = validateAndPrepareData();
-							if (!payload) {
-								e.preventDefault();
-								return;
-							}
-							const form = e.currentTarget.closest('form');
-							if (form) {
-								const input = form.querySelector('input[name="data"]');
-								if (input) {
-									input.value = JSON.stringify(payload);
-								}
-							}
-						}}
 					>
🧹 Nitpick comments (1)
frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/convert-to-quantitative/+page.svelte (1)

229-233: Avoid redefining the global “requirements” i18n key; add the colon in markup instead.

Using m.requirements() is fine; append a colon in the template to avoid colliding with the existing “requirements” key used elsewhere.

-					<div>
-						<strong>{m.requirements()}</strong>
-						{m.probabilityRequirements()}
-					</div>
+					<div>
+						<strong>{m.requirements()}</strong>: {m.probabilityRequirements()}
+					</div>
@@
-					<div>
-						<strong>{m.requirements()}</strong>
-						{m.impactRequirements()}
-					</div>
+					<div>
+						<strong>{m.requirements()}</strong>: {m.impactRequirements()}
+					</div>

Also applies to: 281-284

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24f5af6 and 6419e05.

📒 Files selected for processing (4)
  • frontend/messages/en.json (3 hunks)
  • frontend/messages/fr.json (2 hunks)
  • frontend/src/lib/components/Forms/ModelForm/QuantitativeRiskHypothesisForm.svelte (2 hunks)
  • frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/convert-to-quantitative/+page.svelte (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-02-10T17:32:54.445Z
Learnt from: nas-tabchiche
Repo: intuitem/ciso-assistant-community PR: 1332
File: frontend/src/routes/(app)/(internal)/stored-libraries/[id=uuid]/+page.svelte:0-0
Timestamp: 2025-02-10T17:32:54.445Z
Learning: In SvelteKit applications using form actions with `enhance` and `applyAction`, error handling is managed at the form action level, making additional try/catch blocks in the frontend fetch redundant.

Applied to files:

  • frontend/src/routes/(app)/(internal)/risk-assessments/[id=uuid]/convert-to-quantitative/+page.svelte
🔇 Additional comments (1)
frontend/messages/fr.json (1)

2538-2566: Localization additions look consistent; consider mirroring the EN cleanup.

FR didn’t introduce a second “requirements” key (good). Once EN removes the duplicate, both locales will stay aligned. No blocking issues here.

If you drop the duplicate in EN, re-run the duplicate‑key check script shared in the EN comment to confirm FR stays clean.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
backend/core/views.py (4)

1948-1974: Make loss_threshold optional and accept 0; fix required-fields check.

Doc/design calls loss_threshold optional; current check requires it and rejects 0. Coerce when provided; allow None and 0.

Apply this diff:

-        # Validate payload
+        # Validate payload
         probability_anchors = request.data.get("probability_anchors", [])
         impact_anchors = request.data.get("impact_anchors", [])
         loss_threshold = request.data.get("loss_threshold")
 
-        if not probability_anchors or not impact_anchors or loss_threshold is None:
+        if not probability_anchors or not impact_anchors:
             return Response(
                 {
-                    "detail": "Missing required fields: probability_anchors, impact_anchors, and loss_threshold"
+                    "detail": "Missing required fields: probability_anchors and impact_anchors"
                 },
                 status=status.HTTP_400_BAD_REQUEST,
             )
 
-        # Validate loss_threshold is a positive number
-        try:
-            loss_threshold = float(loss_threshold)
-            if loss_threshold <= 0:
-                return Response(
-                    {"detail": "loss_threshold must be greater than 0"},
-                    status=status.HTTP_400_BAD_REQUEST,
-                )
-        except (TypeError, ValueError):
-            return Response(
-                {"detail": "loss_threshold must be a valid number"},
-                status=status.HTTP_400_BAD_REQUEST,
-            )
+        # Coerce optional loss_threshold if provided (0 is valid)
+        if loss_threshold is not None:
+            try:
+                loss_threshold = float(loss_threshold)
+            except (TypeError, ValueError):
+                return Response(
+                    {"detail": "loss_threshold must be a number"},
+                    status=status.HTTP_400_BAD_REQUEST,
+                )

1975-1994: Cast anchor indices to int; prevent mismatches with scenario.*_proba.

scenario.current_proba/residual_proba are ints; keeping string indices causes lookups to fail.

Apply this diff:

-        probability_map = {}
+        probability_map: dict[int, float] = {}
         try:
             for item in probability_anchors:
-                idx = item["index"]
+                idx = int(item["index"])
                 value = float(item["value"])
                 if not (0 <= value <= 1):
                     return Response(
                         {
                             "detail": f"Probability value must be between 0 and 1, got {value} for index {idx}"
                         },
                         status=status.HTTP_400_BAD_REQUEST,
                     )
                 probability_map[idx] = value
         except (KeyError, TypeError, ValueError) as e:
             return Response(
                 {"detail": f"Invalid probability anchor format: {str(e)}"},
                 status=status.HTTP_400_BAD_REQUEST,
             )

2001-2019: Normalize impact indices to int; add explicit bounds sanity check.

Avoid string/int key mismatch; ensure 0 < lb < ub explicitly.

Apply this diff:

-        impact_map = {}
+        impact_map: dict[int, dict[str, float]] = {}
         try:
             for item in impact_anchors:
-                idx = item["index"]
+                idx = int(item["index"])
                 central_value = float(item["central_value"])
                 if central_value <= 0:
                     return Response(
                         {
                             "detail": f"Impact central value must be positive, got {central_value} for index {idx}"
                         },
                         status=status.HTTP_400_BAD_REQUEST,
                     )
-                impact_map[idx] = {
-                    "lb": central_value * 0.7,
-                    "ub": central_value * 1.43,
-                }
+                lb = central_value * 0.7
+                ub = central_value * 1.43
+                if not (lb > 0 and ub > lb):
+                    return Response(
+                        {"detail": f"Invalid derived bounds for index {idx}"},
+                        status=status.HTTP_400_BAD_REQUEST,
+                    )
+                impact_map[idx] = {"lb": lb, "ub": ub}
         except (KeyError, TypeError, ValueError) as e:
             return Response(
                 {"detail": f"Invalid impact anchor format: {str(e)}"},
                 status=status.HTTP_400_BAD_REQUEST,
             )

2047-2053: Bug: skip condition should use “or”, not “and”.

Comment says “without threats or assets”; current logic only skips when both missing.

-            # Skip scenarios without threats or assets
-            if not scenario.threats.exists() and not scenario.assets.exists():
+            # Skip scenarios without threats or assets
+            if not scenario.threats.exists() or not scenario.assets.exists():
                 logger.info(
-                    f"Skipping scenario '{scenario.name}' - no threats or assets associated"
+                    f"Skipping scenario '{scenario.name}' - no threats or no assets associated"
                 )
                 scenarios_skipped += 1
                 continue
🧹 Nitpick comments (2)
backend/core/views.py (2)

2045-2047: Reduce N+1 queries when copying relations.

Prefetch M2M before looping.

-        # Convert each risk scenario to a quantitative risk scenario
-        for scenario in risk_assessment.risk_scenarios.all():
+        # Convert each risk scenario to a quantitative risk scenario
+        scenarios = risk_assessment.risk_scenarios.all().prefetch_related(
+            "threats", "assets", "vulnerabilities", "owner",
+            "existing_applied_controls", "applied_controls"
+        )
+        for scenario in scenarios:

2132-2142: Avoid long compute in request; consider async or post-commit.

run_simulation can be heavy; move to background job or transaction.on_commit callbacks to keep API snappy and resilient.

Also applies to: 2176-2186

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6419e05 and 22e0147.

📒 Files selected for processing (3)
  • backend/core/views.py (1 hunks)
  • frontend/messages/en.json (3 hunks)
  • frontend/src/lib/components/Forms/ModelForm/QuantitativeRiskHypothesisForm.svelte (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/lib/components/Forms/ModelForm/QuantitativeRiskHypothesisForm.svelte
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-05T09:18:16.194Z
Learnt from: Mohamed-Hacene
Repo: intuitem/ciso-assistant-community PR: 2472
File: backend/iam/models.py:906-907
Timestamp: 2025-09-05T09:18:16.194Z
Learning: In the get_accessible_object_ids method in backend/iam/models.py, for objects with parent_folder attribute (like Folder objects), the folder association logic treats each folder as being associated with itself, so the mapping is folder_id → folder_id. The code `[(fid, fid) for fid in folder_permissions.keys()]` correctly implements this self-referential mapping rather than querying for child folders.

Applied to files:

  • backend/core/views.py
📚 Learning: 2025-08-25T08:51:15.404Z
Learnt from: Mohamed-Hacene
Repo: intuitem/ciso-assistant-community PR: 2422
File: backend/core/serializers.py:1018-1030
Timestamp: 2025-08-25T08:51:15.404Z
Learning: The CISO Assistant project uses a custom permission system where RoleAssignment.get_accessible_object_ids() provides special handling for Permission objects by filtering them by content_type app_label rather than folder hierarchy, since Permission objects don't belong to folders. This allows safe CRUD operations on permissions while preventing privilege escalation by restricting access to only application-specific permissions from allowed apps: "core", "ebios_rm", "tprm", "privacy", "resilience", and "cal".

Applied to files:

  • backend/core/views.py
🧬 Code graph analysis (1)
backend/core/views.py (3)
backend/crq/models.py (4)
  • QuantitativeRiskStudy (23-360)
  • QuantitativeRiskScenario (363-524)
  • QuantitativeRiskHypothesis (527-940)
  • run_simulation (595-698)
backend/core/models.py (19)
  • RiskAssessment (4279-4683)
  • create (4123-4125)
  • risk_scenarios (3844-3845)
  • assets (6701-6702)
  • save (1636-1644)
  • save (1760-1763)
  • save (2733-2735)
  • save (3294-3296)
  • save (3369-3376)
  • save (3551-3555)
  • save (3765-3789)
  • save (4136-4138)
  • save (4273-4276)
  • save (4344-4346)
  • save (5007-5033)
  • save (5156-5162)
  • save (6100-6120)
  • save (6649-6662)
  • applied_controls (6705-6706)
backend/iam/models.py (3)
  • RoleAssignment (807-1094)
  • is_access_allowed (837-857)
  • save (349-356)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: test (3.12)
  • GitHub Check: build_community_frontend
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: build (3.12)
  • GitHub Check: Analyze (python)
🔇 Additional comments (4)
frontend/messages/en.json (2)

2580-2606: ✅ LGTM — Comprehensive and well-structured localization keys for quantitative conversion workflow.

The new keys added (lines 2580–2606) properly support the convert-to-quantitative workflow with clear, non-duplicative messaging. Validation errors are specific and helpful (e.g., "probabilityMustBeBetweenZeroAndHundred", "impactValuesMustBeIncreasing"), help texts explain requirements clearly, and status messages guide users through the Monte Carlo simulation process. No duplicate keys were introduced, and the previous review's concerns about key collisions have been successfully avoided.


2274-2275: ✅ Probability percentage variants added appropriately.

Lines 2274–2275 introduce "probabilityPercent" and "probabilityPercentHelpText" to support percentage-based input (0–100 range), complementing the existing probability keys. The help text clearly distinguishes this from the 0–1 range variant at line 2047.

backend/core/views.py (2)

1933-1936: Good: RBAC-safe object retrieval.

Using self.get_object() enforces the scoped queryset and avoids existence leaks.


2083-2091: No action required — QuantitativeRiskScenario has a folder field through FolderMixin inheritance.

QuantitativeRiskScenario inherits from FolderMixin, which defines a folder foreign key field. The code at lines 2083–2091 correctly passes folder=risk_assessment.folder to the create() method. No keyword errors will occur.

Comment on lines +2109 to +2123
current_hypothesis = QuantitativeRiskHypothesis.objects.create(
quantitative_risk_scenario=quant_scenario,
name="current",
risk_stage="current",
ref_id=scenario.ref_id or "",
folder=risk_assessment.folder,
parameters={
"probability": probability_value,
"impact": {
"distribution": "LOGNORMAL-CI90",
"lb": impact_value["lb"],
"ub": impact_value["ub"],
},
},
)
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

QuantitativeRiskHypothesis likely has no “name” field — creation will crash.

Model summary lists fields; name is absent. Passing name=... will raise TypeError. Drop it.

Apply this diff:

-                current_hypothesis = QuantitativeRiskHypothesis.objects.create(
+                current_hypothesis = QuantitativeRiskHypothesis.objects.create(
                     quantitative_risk_scenario=quant_scenario,
-                    name="current",
                     risk_stage="current",
                     ref_id=scenario.ref_id or "",
                     folder=risk_assessment.folder,
                     parameters={
                         "probability": probability_value,
                         "impact": {
                             "distribution": "LOGNORMAL-CI90",
                             "lb": impact_value["lb"],
                             "ub": impact_value["ub"],
                         },
                     },
                 )
-                residual_hypothesis = QuantitativeRiskHypothesis.objects.create(
+                residual_hypothesis = QuantitativeRiskHypothesis.objects.create(
                     quantitative_risk_scenario=quant_scenario,
-                    name="residual",
                     risk_stage="residual",
                     is_selected=True,
                     ref_id=f"{scenario.ref_id}_RES" if scenario.ref_id else "",
                     folder=risk_assessment.folder,
                     parameters={
                         "probability": residual_probability_value,
                         "impact": {
                             "distribution": "LOGNORMAL-CI90",
                             "lb": residual_impact_value["lb"],
                             "ub": residual_impact_value["ub"],
                         },
                     },
                 )

Also applies to: 2148-2163

🤖 Prompt for AI Agents
In backend/core/views.py around lines 2109-2123 (and similarly lines 2148-2163),
the QuantitativeRiskHypothesis.objects.create calls include a name=... keyword
that does not exist on the model and will raise a TypeError; remove the name=...
argument from both create calls so only valid model fields
(quantitative_risk_scenario, risk_stage, ref_id, folder, parameters, etc.) are
passed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
backend/core/views.py (1)

2053-2057: Bug: skip condition should be “or”, not “and”.

Comment says “without threats or assets” but code only skips when both are missing.

-            # Skip scenarios without threats or assets
-            if not scenario.threats.exists() and not scenario.assets.exists():
+            # Skip scenarios without threats or assets
+            if not scenario.threats.exists() or not scenario.assets.exists():
🧹 Nitpick comments (1)
backend/core/views.py (1)

2138-2147: Surface simulation failures and report them in the response.

Currently failures are only logged; API always counts the scenario as converted. Track failures and return them so clients can react (and optionally don’t count failed ones as converted).

-        scenarios_converted = 0
-        scenarios_skipped = 0
+        scenarios_converted = 0
+        scenarios_skipped = 0
+        scenarios_failed = 0
@@
-                except Exception as e:
-                    logger.error(
+                except Exception as e:
+                    scenarios_failed += 1
+                    logger.error(
                         f"Failed to run simulation for scenario {scenario.name}: {str(e)}"
                     )
@@
-                except Exception as e:
-                    logger.error(
+                except Exception as e:
+                    scenarios_failed += 1
+                    logger.error(
                         f"Failed to run simulation for residual scenario {scenario.name}: {str(e)}"
                     )
@@
-        return Response(
+        return Response(
             {
                 "message": "Conversion successful",
                 "quantitative_risk_study_id": str(quantitative_study.id),
                 "scenarios_converted": scenarios_converted,
                 "scenarios_skipped": scenarios_skipped,
+                "scenarios_failed": scenarios_failed,
             },
             status=status.HTTP_201_CREATED,
         )

Also applies to: 2183-2191, 2199-2207

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 22e0147 and bdf5b39.

📒 Files selected for processing (1)
  • backend/core/views.py (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-05T09:18:16.194Z
Learnt from: Mohamed-Hacene
Repo: intuitem/ciso-assistant-community PR: 2472
File: backend/iam/models.py:906-907
Timestamp: 2025-09-05T09:18:16.194Z
Learning: In the get_accessible_object_ids method in backend/iam/models.py, for objects with parent_folder attribute (like Folder objects), the folder association logic treats each folder as being associated with itself, so the mapping is folder_id → folder_id. The code `[(fid, fid) for fid in folder_permissions.keys()]` correctly implements this self-referential mapping rather than querying for child folders.

Applied to files:

  • backend/core/views.py
📚 Learning: 2025-08-25T08:51:15.404Z
Learnt from: Mohamed-Hacene
Repo: intuitem/ciso-assistant-community PR: 2422
File: backend/core/serializers.py:1018-1030
Timestamp: 2025-08-25T08:51:15.404Z
Learning: The CISO Assistant project uses a custom permission system where RoleAssignment.get_accessible_object_ids() provides special handling for Permission objects by filtering them by content_type app_label rather than folder hierarchy, since Permission objects don't belong to folders. This allows safe CRUD operations on permissions while preventing privilege escalation by restricting access to only application-specific permissions from allowed apps: "core", "ebios_rm", "tprm", "privacy", "resilience", and "cal".

Applied to files:

  • backend/core/views.py
🧬 Code graph analysis (1)
backend/core/views.py (3)
backend/crq/models.py (4)
  • QuantitativeRiskStudy (23-360)
  • QuantitativeRiskScenario (363-524)
  • QuantitativeRiskHypothesis (527-940)
  • run_simulation (595-698)
backend/core/models.py (18)
  • RiskAssessment (4279-4683)
  • risk_scenarios (3844-3845)
  • assets (6701-6702)
  • save (1636-1644)
  • save (1760-1763)
  • save (2733-2735)
  • save (3294-3296)
  • save (3369-3376)
  • save (3551-3555)
  • save (3765-3789)
  • save (4136-4138)
  • save (4273-4276)
  • save (4344-4346)
  • save (5007-5033)
  • save (5156-5162)
  • save (6100-6120)
  • save (6649-6662)
  • applied_controls (6705-6706)
backend/iam/models.py (3)
  • RoleAssignment (807-1094)
  • is_access_allowed (837-857)
  • save (349-356)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: enterprise-startup-functional-test (3.12)
  • GitHub Check: enterprise-startup-docker-compose-test
  • GitHub Check: startup-functional-test (3.12)
  • GitHub Check: startup-docker-compose-test
  • GitHub Check: Analyze (python)
  • GitHub Check: build_community_frontend
  • GitHub Check: build_enterprise_frontend
  • GitHub Check: build (3.12)
  • GitHub Check: test (3.12)
🔇 Additional comments (1)
backend/core/views.py (1)

1933-1941: Good RBAC: get_object() enforces scoped queryset and permissions.

Replacing direct ORM fetch with self.get_object() is correct and prevents info disclosure.

Comment on lines +1976 to +1997
probability_map = {}
try:
for item in probability_anchors:
idx = item["index"]
value = float(item["value"])
if not (0 <= value <= 1):
return Response(
{
"detail": f"Probability value must be between 0 and 1, got {value} for index {idx}"
},
status=status.HTTP_400_BAD_REQUEST,
)
probability_map[idx] = value
except (KeyError, TypeError, ValueError) as e:
logger.warning(f"Invalid probability anchor format: {e}")
return Response(
{
"detail": "Invalid probability anchor format. Each anchor must have 'index' and 'value' fields, with value between 0 and 1."
},
status=status.HTTP_400_BAD_REQUEST,
)

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 | 🟠 Major

Coerce anchor index to int to avoid silent skips (key type mismatch).

idx is taken as-is from JSON; if frontend sends "0" (string), membership checks like current_proba in probability_map will fail. Cast index to int when building both maps.

-        for item in probability_anchors:
-            idx = item["index"]
+        for item in probability_anchors:
+            idx = int(item["index"])
             value = float(item["value"])
             if not (0 <= value <= 1):
                 return Response(
@@
-        for item in impact_anchors:
-            idx = item["index"]
+        for item in impact_anchors:
+            idx = int(item["index"])
             central_value = float(item["central_value"])
             if central_value <= 0:
                 return Response(

Also applies to: 2003-2018

🤖 Prompt for AI Agents
In backend/core/views.py around lines 1976 to 1997 (and similarly at 2003–2018),
the anchor index is taken directly from JSON which can be a string and later
cause key-mismatch lookups; coerce the index to an int when building the
probability_map (e.g., idx = int(item["index"])) so all map keys are integers,
and let the existing except block continue to catch ValueError/TypeError if
conversion fails; apply the same int coercion to the other map-building block at
2003–2018 to ensure consistent key types.

@ab-smith ab-smith merged commit c3f9083 into main Nov 5, 2025
115 of 117 checks passed
@ab-smith ab-smith deleted the quali_to_quanti_converter branch November 5, 2025 23:06
@github-actions github-actions bot locked and limited conversation to collaborators Nov 5, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants