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
82 changes: 41 additions & 41 deletions limacharlie/commands/case_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
lifecycle including who made each change and when.

Examples:
limacharlie case get --id 42
limacharlie case get --case-number 42
"""

_EXPLAIN_UPDATE = """\
Expand All @@ -96,10 +96,10 @@
closed -> in_progress (reopen)

Examples:
limacharlie case update --id 42 --status in_progress
limacharlie case update --id 42 --severity high
limacharlie case update --id 42 --assignees alice@example.com
limacharlie case update --id 42 --status resolved \\
limacharlie case update --case-number 42 --status in_progress
limacharlie case update --case-number 42 --severity high
limacharlie case update --case-number 42 --assignees alice@example.com
limacharlie case update --case-number 42 --status resolved \\
--classification true_positive \\
--conclusion "Contained via network isolation"
"""
Expand All @@ -120,10 +120,10 @@
Provide content via --content, --input-file, or stdin.

Examples:
limacharlie case add-note --id 42 --content "Initial triage complete"
limacharlie case add-note --id 42 --type analysis \\
limacharlie case add-note --case-number 42 --content "Initial triage complete"
limacharlie case add-note --case-number 42 --type analysis \\
--content "Confirmed C2 beacon to 10.0.0.1"
echo "Handoff notes" | limacharlie case add-note --id 42 --type handoff
echo "Handoff notes" | limacharlie case add-note --case-number 42 --type handoff
"""

_EXPLAIN_BULK_UPDATE = """\
Expand Down Expand Up @@ -397,9 +397,9 @@
are skipped.

Examples:
limacharlie case export --id 42
limacharlie case export --id 42 --output json > case.json
limacharlie case export --id 42 --with-data ./case-export
limacharlie case export --case-number 42
limacharlie case export --case-number 42 --output json > case.json
limacharlie case export --case-number 42 --with-data ./case-export
"""

_EXPLAIN_CREATE = """\
Expand Down Expand Up @@ -463,26 +463,26 @@
replaced with the provided set. Use --tag/-t for each tag value.

Examples:
limacharlie case tag set --id 42 --tag phishing
limacharlie case tag set --id 42 -t phishing -t urgent
limacharlie case tag set --case-number 42 --tag phishing
limacharlie case tag set --case-number 42 -t phishing -t urgent
"""

_EXPLAIN_TAG_ADD = """\
Add one or more tags to a case, merging with any existing tags.
Duplicate tags are automatically deduplicated.

Examples:
limacharlie case tag add --id 42 --tag new-tag
limacharlie case tag add --id 42 -t phishing -t urgent
limacharlie case tag add --case-number 42 --tag new-tag
limacharlie case tag add --case-number 42 -t phishing -t urgent
"""

_EXPLAIN_TAG_REMOVE = """\
Remove one or more tags from a case. Tags not currently on the
case are silently ignored.

Examples:
limacharlie case tag remove --id 42 --tag old-tag
limacharlie case tag remove --id 42 -t phishing -t urgent
limacharlie case tag remove --case-number 42 --tag old-tag
limacharlie case tag remove --case-number 42 -t phishing -t urgent
"""

register_explain("case.tag.set", _EXPLAIN_TAG_SET)
Expand Down Expand Up @@ -647,13 +647,13 @@ def list_cases(ctx, status, severity, classification, assignee, search,
# ---------------------------------------------------------------------------

@group.command()
@click.option("--id", "case_number", required=True, type=int, help="Case number.")
@click.option("--case-number", "case_number", required=True, type=int, help="Case number.")
@pass_context
def get(ctx, case_number) -> None:
"""Get a case with its event timeline.

Example:
limacharlie case get --id 42
limacharlie case get --case-number 42
"""
t = _get_cases(ctx)
data = t.get_case(case_number)
Expand All @@ -665,7 +665,7 @@ def get(ctx, case_number) -> None:
# ---------------------------------------------------------------------------

@group.command()
@click.option("--id", "case_number", required=True, type=int, help="Case number.")
@click.option("--case-number", "case_number", required=True, type=int, help="Case number.")
@click.option("--with-data", "output_dir", default=None, type=click.Path(),
help="Export with full data to a directory.")
@pass_context
Expand All @@ -678,8 +678,8 @@ def export(ctx, case_number, output_dir) -> None:
artifact binaries.

Examples:
limacharlie case export --id 42
limacharlie case export --id 42 --with-data ./out
limacharlie case export --case-number 42
limacharlie case export --case-number 42 --with-data ./out
"""
t = _get_cases(ctx)
data = t.export_case(case_number)
Expand Down Expand Up @@ -780,7 +780,7 @@ def _export_with_data(ctx: click.Context, t: Cases, data: dict[str, Any],
# ---------------------------------------------------------------------------

@group.command()
@click.option("--id", "case_number", required=True, type=int, help="Case number.")
@click.option("--case-number", "case_number", required=True, type=int, help="Case number.")
@click.option("--status", default=None, type=_STATUS_CHOICES, help="New status.")
@click.option("--severity", default=None, type=_SEVERITY_CHOICES, help="Case severity (critical, high, medium, low, info).")
@click.option("--assignees", multiple=True, help="Assignee email (repeatable for multiple assignees).")
Expand All @@ -796,12 +796,12 @@ def update(ctx, case_number, status, severity, assignees, classification,
Only provided fields are changed.

Examples:
limacharlie case update --id 42 --status in_progress
limacharlie case update --id 42 --severity high
limacharlie case update --id 42 --assignees alice@example.com
limacharlie case update --id 42 --status resolved \\
limacharlie case update --case-number 42 --status in_progress
limacharlie case update --case-number 42 --severity high
limacharlie case update --case-number 42 --assignees alice@example.com
limacharlie case update --case-number 42 --status resolved \\
--classification true_positive
limacharlie case update --id 42 --tag phishing --tag urgent
limacharlie case update --case-number 42 --tag phishing --tag urgent
"""
fields = {
"status": status,
Expand Down Expand Up @@ -829,7 +829,7 @@ def update(ctx, case_number, status, severity, assignees, classification,
# ---------------------------------------------------------------------------

@group.command("add-note")
@click.option("--id", "case_number", required=True, type=int, help="Case number.")
@click.option("--case-number", "case_number", required=True, type=int, help="Case number.")
@click.option("--content", default=None, help="Note content.")
@click.option("--input-file", default=None, type=click.Path(exists=True), help="Read note content from file.")
@click.option("--type", "note_type", default=None, type=_NOTE_TYPE_CHOICES,
Expand All @@ -841,10 +841,10 @@ def add_note(ctx, case_number, content, input_file, note_type) -> None:
Provide content via --content, --input-file, or stdin.

Examples:
limacharlie case add-note --id 42 --content "Triage complete"
limacharlie case add-note --id 42 --type analysis \\
limacharlie case add-note --case-number 42 --content "Triage complete"
limacharlie case add-note --case-number 42 --type analysis \\
--content "Confirmed C2 beacon"
echo "notes" | limacharlie case add-note --id 42
echo "notes" | limacharlie case add-note --case-number 42
"""
if content:
text = content
Expand Down Expand Up @@ -1423,31 +1423,31 @@ def tag_group() -> None:


@tag_group.command("set")
@click.option("--id", "case_number", required=True, type=int, help="Case number.")
@click.option("--case-number", "case_number", required=True, type=int, help="Case number.")
@click.option("--tag", "-t", "tags", multiple=True, required=True, help="Tag value (repeatable).")
@pass_context
def tag_set(ctx, case_number, tags) -> None:
"""Replace all tags on a case.

Examples:
limacharlie case tag set --id 42 --tag phishing
limacharlie case tag set --id 42 -t phishing -t urgent
limacharlie case tag set --case-number 42 --tag phishing
limacharlie case tag set --case-number 42 -t phishing -t urgent
"""
t = _get_cases(ctx)
data = t.update_case(case_number, tags=list(tags))
_output(ctx, data)


@tag_group.command("add")
@click.option("--id", "case_number", required=True, type=int, help="Case number.")
@click.option("--case-number", "case_number", required=True, type=int, help="Case number.")
@click.option("--tag", "-t", "tags", multiple=True, required=True, help="Tag value to add (repeatable).")
@pass_context
def tag_add(ctx, case_number, tags) -> None:
"""Add tags to a case (merged with existing).

Examples:
limacharlie case tag add --id 42 --tag new-tag
limacharlie case tag add --id 42 -t phishing -t urgent
limacharlie case tag add --case-number 42 --tag new-tag
limacharlie case tag add --case-number 42 -t phishing -t urgent
"""
tk = _get_cases(ctx)
current = tk.get_case(case_number)
Expand All @@ -1463,15 +1463,15 @@ def tag_add(ctx, case_number, tags) -> None:


@tag_group.command("remove")
@click.option("--id", "case_number", required=True, type=int, help="Case number.")
@click.option("--case-number", "case_number", required=True, type=int, help="Case number.")
@click.option("--tag", "-t", "tags", multiple=True, required=True, help="Tag value to remove (repeatable).")
@pass_context
def tag_remove(ctx, case_number, tags) -> None:
"""Remove tags from a case.

Examples:
limacharlie case tag remove --id 42 --tag old-tag
limacharlie case tag remove --id 42 -t phishing -t urgent
limacharlie case tag remove --case-number 42 --tag old-tag
limacharlie case tag remove --case-number 42 -t phishing -t urgent
"""
tk = _get_cases(ctx)
current = tk.get_case(case_number)
Expand Down
8 changes: 5 additions & 3 deletions limacharlie/sdk/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ def create_case(
"""
data: dict[str, Any] = {}
if detection is not None:
# The extension schema declares detection as type "json",
# which the platform validates as a JSON string (not a dict).
data["detection"] = json.dumps(detection)
# Pass the detection dict directly — the gzdata encoding
# already JSON-serializes the full data dict, so json.dumps
# here would double-encode it into a string that the LC
# backend drops (schema type "json" expects an object).
data["detection"] = detection
if severity is not None:
data["severity"] = severity
if summary is not None:
Expand Down
Loading