From 27b1755927589ad75f17524941392e9ff4ae4116 Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Sun, 29 Mar 2026 16:21:43 -0700 Subject: [PATCH] Make --summary required for case create CLI command Cases require summaries, so the CLI now enforces this at invocation time rather than allowing empty summaries. Co-Authored-By: Claude Opus 4.6 (1M context) --- limacharlie/commands/case_cmd.py | 23 ++++++++++++----------- tests/unit/test_cli_case.py | 29 +++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/limacharlie/commands/case_cmd.py b/limacharlie/commands/case_cmd.py index 09e3d9c..a806ecd 100644 --- a/limacharlie/commands/case_cmd.py +++ b/limacharlie/commands/case_cmd.py @@ -416,17 +416,17 @@ in the detection object (or defaults to 'medium'). Valid severities: critical, high, medium, low, info. -Use --summary to set an initial case summary at creation time. When -provided, the summary is included in the 'created' audit event so -D&R rules and webhooks can act on it immediately. +--summary is required. The summary is included in the 'created' audit +event so D&R rules and webhooks can act on it immediately. Examples: - limacharlie case create --detection '' + limacharlie case create --summary "Investigating lateral movement" limacharlie case create --detection '' \\ - --severity high + --summary "Triage detection" + limacharlie case create --detection '' \\ + --severity high --summary "High severity lateral movement" limacharlie case create --severity medium \\ --summary "Investigating lateral movement" - limacharlie case create """ register_explain("case.create", _EXPLAIN_CREATE) @@ -568,18 +568,19 @@ def group() -> None: help="Full detection JSON object (optional).") @click.option("--severity", default=None, type=_SEVERITY_CHOICES, help="Case severity override (default: derived from detection).") -@click.option("--summary", default=None, - help="Case summary set at creation time (max 8192 chars).") +@click.option("--summary", required=True, + help="Case summary (required, max 8192 chars).") @pass_context def create(ctx, detection_json, severity, summary) -> None: """Create a new case, optionally from a detection. Examples: - limacharlie case create --detection '' + limacharlie case create --summary "Investigating lateral movement" + limacharlie case create --detection '' \\ + --summary "Triage detection" limacharlie case create --detection '' \\ - --severity high + --severity high --summary "High severity lateral movement" limacharlie case create --severity medium --summary "Investigating lateral movement" - limacharlie case create """ detection = None if detection_json is not None: diff --git a/tests/unit/test_cli_case.py b/tests/unit/test_cli_case.py index e0057c1..8b67d8b 100644 --- a/tests/unit/test_cli_case.py +++ b/tests/unit/test_cli_case.py @@ -115,7 +115,8 @@ def test_create_with_detection(self): p1, p2, p3 = _patch_cases() with p1, p2, p3 as mock_t_cls: result, mock_t = _invoke( - ["case", "create", "--detection", self._SAMPLE_DETECTION], + ["case", "create", "--detection", self._SAMPLE_DETECTION, + "--summary", "Triage detection"], mock_t_cls, return_value={"created": 1, "case_id": "tid-new"}, ) @@ -123,7 +124,7 @@ def test_create_with_detection(self): mock_t.create_case.assert_called_once_with( json.loads(self._SAMPLE_DETECTION), severity=None, - summary=None, + summary="Triage detection", ) def test_create_with_severity_override(self): @@ -132,7 +133,8 @@ def test_create_with_severity_override(self): result, mock_t = _invoke( ["case", "create", "--detection", self._SAMPLE_DETECTION, - "--severity", "critical"], + "--severity", "critical", + "--summary", "Critical lateral movement"], mock_t_cls, return_value={"created": 1, "case_id": "tid-new"}, ) @@ -140,14 +142,14 @@ def test_create_with_severity_override(self): mock_t.create_case.assert_called_once_with( json.loads(self._SAMPLE_DETECTION), severity="critical", - summary=None, + summary="Critical lateral movement", ) def test_create_without_detection(self): p1, p2, p3 = _patch_cases() with p1, p2, p3 as mock_t_cls: result, mock_t = _invoke( - ["case", "create"], + ["case", "create", "--summary", "Manual investigation"], mock_t_cls, return_value={"created": 1, "case_id": "tid-new"}, ) @@ -155,14 +157,15 @@ def test_create_without_detection(self): mock_t.create_case.assert_called_once_with( None, severity=None, - summary=None, + summary="Manual investigation", ) def test_create_without_detection_with_severity(self): p1, p2, p3 = _patch_cases() with p1, p2, p3 as mock_t_cls: result, mock_t = _invoke( - ["case", "create", "--severity", "medium"], + ["case", "create", "--severity", "medium", + "--summary", "Medium severity case"], mock_t_cls, return_value={"created": 1, "case_id": "tid-new"}, ) @@ -170,7 +173,7 @@ def test_create_without_detection_with_severity(self): mock_t.create_case.assert_called_once_with( None, severity="medium", - summary=None, + summary="Medium severity case", ) def test_create_with_summary(self): @@ -188,12 +191,21 @@ def test_create_with_summary(self): summary="Lateral movement detected", ) + def test_create_without_summary_rejected(self): + runner = CliRunner() + result = runner.invoke(cli, [ + "case", "create", + "--detection", self._SAMPLE_DETECTION, + ]) + assert result.exit_code != 0 + def test_create_invalid_severity_rejected(self): runner = CliRunner() result = runner.invoke(cli, [ "case", "create", "--detection", self._SAMPLE_DETECTION, "--severity", "extreme", + "--summary", "Test", ]) assert result.exit_code != 0 @@ -201,6 +213,7 @@ def test_create_invalid_json_rejected(self): runner = CliRunner() result = runner.invoke(cli, [ "case", "create", "--detection", "not-json", + "--summary", "Test", ]) assert result.exit_code != 0