Skip to content
Merged
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
23 changes: 12 additions & 11 deletions limacharlie/commands/case_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<full detection JSON>'
limacharlie case create --summary "Investigating lateral movement"
limacharlie case create --detection '<full detection JSON>' \\
--severity high
--summary "Triage detection"
limacharlie case create --detection '<full detection JSON>' \\
--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)
Expand Down Expand Up @@ -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 '<full detection JSON>'
limacharlie case create --summary "Investigating lateral movement"
limacharlie case create --detection '<full detection JSON>' \\
--summary "Triage detection"
limacharlie case create --detection '<full detection JSON>' \\
--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:
Expand Down
29 changes: 21 additions & 8 deletions tests/unit/test_cli_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,16 @@ 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"},
)
assert result.exit_code == 0
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):
Expand All @@ -132,45 +133,47 @@ 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"},
)
assert result.exit_code == 0
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"},
)
assert result.exit_code == 0
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"},
)
assert result.exit_code == 0
mock_t.create_case.assert_called_once_with(
None,
severity="medium",
summary=None,
summary="Medium severity case",
)

def test_create_with_summary(self):
Expand All @@ -188,19 +191,29 @@ 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

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

Expand Down