From 4fad8a35611afb58116d53bbffd73d58ce889ff1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Dec 2025 13:14:25 +0000 Subject: [PATCH 1/2] Enhance CLI interface with comprehensive context management and rapid note-taking features Added CLI features for rapid observation logging during live forensic analysis: Context Management: - --show-context: Display active case and evidence - --list: List all cases and evidence in hierarchy - --switch-case: Change active case (by ID or case number) - --switch-evidence: Change active evidence (by ID or name) Case/Evidence Creation: - --new-case: Create new case with optional --name and --investigator - --new-evidence: Create new evidence with optional --description Enhanced Note-Taking: - --stdin: Read note content from stdin for piping command output - --case: Add note to specific case without changing active context - --evidence: Add note to specific evidence without changing active context All context switching commands support both UUIDs and human-friendly identifiers (case numbers for cases, names for evidence). Creating new items automatically sets them as active. Override flags allow temporary context changes without modifying the active state. Updated CLAUDE.md with comprehensive CLI command examples organized by workflow. --- CLAUDE.md | 69 +++++++++-- trace/cli.py | 339 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 366 insertions(+), 42 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e7b5539..939496a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,20 +9,75 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Development Commands ### Running the Application + +#### Launching TUI ```bash -# Run directly from source +# Launch TUI (default behavior) python3 main.py -# Quick CLI note addition (requires active case/evidence set in TUI) -python3 main.py "Your note content here" - -# Export to markdown -python3 main.py --export --output report.md - # Open TUI directly at active case/evidence python3 main.py --open ``` +#### Context Management +```bash +# Show current active case and evidence +python3 main.py --show-context + +# List all cases and evidence in hierarchy +python3 main.py --list + +# Switch active case (by case number or UUID) +python3 main.py --switch-case 2024-001 + +# Switch active evidence (by name or UUID, within active case) +python3 main.py --switch-evidence "disk-image-1" +``` + +#### Creating Cases and Evidence +```bash +# Create new case (automatically sets as active) +python3 main.py --new-case 2024-001 + +# Create case with metadata +python3 main.py --new-case 2024-001 --name "Ransomware Investigation" --investigator "John Doe" + +# Create evidence in active case (automatically sets as active) +python3 main.py --new-evidence "Laptop HDD" + +# Create evidence with description +python3 main.py --new-evidence "Server Logs" --description "Apache access logs from compromised server" +``` + +#### Adding Notes +```bash +# Quick note to active context +python3 main.py "Observed suspicious process at 14:32" + +# Read note from stdin (for piping command output) +echo "Network spike detected" | python3 main.py --stdin +ps aux | grep malware | python3 main.py --stdin +tail -f logfile.txt | grep error | python3 main.py --stdin + +# Add note to specific case without changing active context +python3 main.py --case 2024-002 "Found malware in temp folder" + +# Add note to specific evidence without changing active context +python3 main.py --evidence "disk-image-2" "Bad sectors detected" + +# Add note to specific case and evidence (both overrides) +python3 main.py --case 2024-001 --evidence "Laptop HDD" "Recovered deleted files" +``` + +#### Export +```bash +# Export all data to markdown +python3 main.py --export --output report.md + +# Export with default filename (trace_export.md) +python3 main.py --export +``` + ### Building Binary ```bash # Install dependencies first diff --git a/trace/cli.py b/trace/cli.py index 821d09b..79a6f64 100644 --- a/trace/cli.py +++ b/trace/cli.py @@ -1,11 +1,204 @@ import argparse import sys import time -from .models import Note, Case +from typing import Optional, Tuple +from .models import Note, Case, Evidence from .storage import Storage, StateManager from .crypto import Crypto -def quick_add_note(content: str): +def find_case(storage: Storage, identifier: str) -> Optional[Case]: + """Find a case by case_id (UUID) or case_number.""" + for case in storage.cases: + if case.case_id == identifier or case.case_number == identifier: + return case + return None + +def find_evidence(case: Case, identifier: str) -> Optional[Evidence]: + """Find evidence by evidence_id (UUID) or name within a case.""" + for evidence in case.evidence: + if evidence.evidence_id == identifier or evidence.name == identifier: + return evidence + return None + +def show_context(): + """Display the current active context.""" + state_manager = StateManager() + storage = Storage() + + state = state_manager.get_active() + case_id = state.get("case_id") + evidence_id = state.get("evidence_id") + + if not case_id: + print("No active context set.") + print("Use --switch-case to set an active case, or open the TUI to select one.") + return + + case = storage.get_case(case_id) + if not case: + print("Error: Active case not found in storage.") + return + + print(f"Active context:") + print(f" Case: {case.case_number}", end="") + if case.name: + print(f" - {case.name}", end="") + print(f" [{case.case_id[:8]}...]") + + if evidence_id: + evidence = find_evidence(case, evidence_id) + if evidence: + print(f" Evidence: {evidence.name}", end="") + if evidence.description: + print(f" - {evidence.description}", end="") + print(f" [{evidence.evidence_id[:8]}...]") + else: + print(f" Evidence: [not found - stale reference]") + else: + print(f" Evidence: [none - notes will attach to case]") + +def list_contexts(): + """List all cases and their evidence in a hierarchical format.""" + storage = Storage() + + if not storage.cases: + print("No cases found.") + print("Use --new-case to create one, or open the TUI.") + return + + print("Cases and Evidence:") + for case in storage.cases: + # Show case + print(f" [{case.case_id[:8]}...] {case.case_number}", end="") + if case.name: + print(f" - {case.name}", end="") + if case.investigator: + print(f" (Investigator: {case.investigator})", end="") + print() + + # Show evidence under this case + for evidence in case.evidence: + print(f" [{evidence.evidence_id[:8]}...] {evidence.name}", end="") + if evidence.description: + print(f" - {evidence.description}", end="") + print() + + # Add blank line between cases for readability + if storage.cases[-1] != case: + print() + +def create_case(case_number: str, name: Optional[str] = None, investigator: Optional[str] = None): + """Create a new case and set it as active.""" + storage = Storage() + state_manager = StateManager() + + # Check if case number already exists + existing = find_case(storage, case_number) + if existing: + print(f"Error: Case with number '{case_number}' already exists.", file=sys.stderr) + sys.exit(1) + + # Create new case + case = Case(case_number=case_number, name=name, investigator=investigator) + storage.cases.append(case) + storage.save_data() + + # Set as active case + state_manager.set_active(case.case_id, None) + + print(f"✓ Created case '{case_number}' [{case.case_id[:8]}...]") + if name: + print(f" Name: {name}") + if investigator: + print(f" Investigator: {investigator}") + print(f"✓ Set as active case") + +def create_evidence(name: str, description: Optional[str] = None): + """Create new evidence and attach to active case.""" + storage = Storage() + state_manager = StateManager() + + state = state_manager.get_active() + case_id = state.get("case_id") + + if not case_id: + print("Error: No active case set. Use --switch-case or --new-case first.", file=sys.stderr) + sys.exit(1) + + case = storage.get_case(case_id) + if not case: + print("Error: Active case not found in storage.", file=sys.stderr) + sys.exit(1) + + # Check if evidence with this name already exists in the case + existing = find_evidence(case, name) + if existing: + print(f"Error: Evidence named '{name}' already exists in case '{case.case_number}'.", file=sys.stderr) + sys.exit(1) + + # Create new evidence + evidence = Evidence(name=name, description=description) + case.evidence.append(evidence) + storage.save_data() + + # Set as active evidence + state_manager.set_active(case.case_id, evidence.evidence_id) + + print(f"✓ Created evidence '{name}' [{evidence.evidence_id[:8]}...]") + if description: + print(f" Description: {description}") + print(f"✓ Added to case '{case.case_number}'") + print(f"✓ Set as active evidence") + +def switch_case(identifier: str): + """Switch active case context.""" + storage = Storage() + state_manager = StateManager() + + case = find_case(storage, identifier) + if not case: + print(f"Error: Case '{identifier}' not found.", file=sys.stderr) + print("Use --list to see available cases.", file=sys.stderr) + sys.exit(1) + + # Set as active case, clear evidence + state_manager.set_active(case.case_id, None) + + print(f"✓ Switched to case '{case.case_number}' [{case.case_id[:8]}...]") + if case.name: + print(f" {case.name}") + +def switch_evidence(identifier: str): + """Switch active evidence context within the active case.""" + storage = Storage() + state_manager = StateManager() + + state = state_manager.get_active() + case_id = state.get("case_id") + + if not case_id: + print("Error: No active case set. Use --switch-case first.", file=sys.stderr) + sys.exit(1) + + case = storage.get_case(case_id) + if not case: + print("Error: Active case not found in storage.", file=sys.stderr) + sys.exit(1) + + evidence = find_evidence(case, identifier) + if not evidence: + print(f"Error: Evidence '{identifier}' not found in case '{case.case_number}'.", file=sys.stderr) + print("Use --list to see available evidence.", file=sys.stderr) + sys.exit(1) + + # Set as active evidence + state_manager.set_active(case.case_id, evidence.evidence_id) + + print(f"✓ Switched to evidence '{evidence.name}' [{evidence.evidence_id[:8]}...]") + if evidence.description: + print(f" {evidence.description}") + +def quick_add_note(content: str, case_override: Optional[str] = None, evidence_override: Optional[str] = None): storage = Storage() state_manager = StateManager() @@ -17,31 +210,43 @@ def quick_add_note(content: str): state = state_manager.get_active() settings = state_manager.get_settings() - case_id = state.get("case_id") - evidence_id = state.get("evidence_id") + # Handle case override or use active case + if case_override: + case = find_case(storage, case_override) + if not case: + print(f"Error: Case '{case_override}' not found.", file=sys.stderr) + print("Use --list to see available cases.", file=sys.stderr) + sys.exit(1) + else: + case_id = state.get("case_id") + if not case_id: + print("Error: No active case set. Use --switch-case, --new-case, or open the TUI to select a case.", file=sys.stderr) + sys.exit(1) - if not case_id: - print("Error: No active case set. Open the TUI to select a case first.", file=sys.stderr) - sys.exit(1) - - case = storage.get_case(case_id) - if not case: - print("Error: Active case not found in storage. Ensure you have set an active case in the TUI.", file=sys.stderr) - sys.exit(1) + case = storage.get_case(case_id) + if not case: + print("Error: Active case not found in storage.", file=sys.stderr) + sys.exit(1) + # Handle evidence override or use active evidence target_evidence = None - if evidence_id: - # Find and validate evidence belongs to active case - for ev in case.evidence: - if ev.evidence_id == evidence_id: - target_evidence = ev - break - + if evidence_override: + target_evidence = find_evidence(case, evidence_override) if not target_evidence: - # Evidence ID is set but doesn't exist in case - clear it - print(f"Warning: Active evidence not found in case. Clearing to case level.", file=sys.stderr) - state_manager.set_active(case_id, None) + print(f"Error: Evidence '{evidence_override}' not found in case '{case.case_number}'.", file=sys.stderr) + print("Use --list to see available evidence.", file=sys.stderr) + sys.exit(1) + elif not case_override: # Only use active evidence if not overriding case + evidence_id = state.get("evidence_id") + if evidence_id: + # Find and validate evidence belongs to active case + target_evidence = find_evidence(case, evidence_id) + + if not target_evidence: + # Evidence ID is set but doesn't exist in case - clear it + print(f"Warning: Active evidence not found in case. Clearing to case level.", file=sys.stderr) + state_manager.set_active(case.case_id, None) # Create note note = Note(content=content) @@ -67,10 +272,6 @@ def quick_add_note(content: str): if target_evidence: target_evidence.notes.append(note) print(f"✓ Note added to evidence '{target_evidence.name}'") - elif evidence_id: - print("Warning: Active evidence not found. Adding to case instead.") - case.notes.append(note) - print(f"✓ Note added to case '{case.case_number}'") else: case.notes.append(note) print(f"✓ Note added to case '{case.case_number}'") @@ -182,25 +383,93 @@ def format_note_for_export(note: Note) -> str: return "".join(lines) def main(): - parser = argparse.ArgumentParser(description="trace: Forensic Note Taking Tool") - parser.add_argument("note", nargs="?", help="Quick note content to add to active context") - parser.add_argument("--export", help="Export all data to Markdown file", action="store_true") - parser.add_argument("--output", help="Output file for export", default="trace_export.md") - parser.add_argument("--open", "-o", help="Open TUI directly at active case/evidence", action="store_true") + parser = argparse.ArgumentParser( + description="trace: Forensic Note Taking Tool", + epilog="Examples:\n" + " trace 'Found suspicious process' Add note to active context\n" + " trace --stdin < output.txt Add file contents as note\n" + " trace --list List all cases and evidence\n" + " trace --new-case 2024-001 Create new case\n" + " trace --switch-case 2024-001 Switch active case\n", + formatter_class=argparse.RawDescriptionHelpFormatter + ) - # We will import TUI only if needed to keep start time fast + # Note content (positional or stdin) + parser.add_argument("note", nargs="?", help="Quick note content to add to active context") + parser.add_argument("--stdin", action="store_true", help="Read note content from stdin") + + # Context management + parser.add_argument("--show-context", action="store_true", help="Show active case and evidence") + parser.add_argument("--list", action="store_true", help="List all cases and evidence") + parser.add_argument("--switch-case", metavar="IDENTIFIER", help="Switch active case (by ID or case number)") + parser.add_argument("--switch-evidence", metavar="IDENTIFIER", help="Switch active evidence (by ID or name)") + + # Temporary overrides for note addition + parser.add_argument("--case", metavar="IDENTIFIER", help="Use specific case for this note (doesn't change active)") + parser.add_argument("--evidence", metavar="IDENTIFIER", help="Use specific evidence for this note (doesn't change active)") + + # Case and evidence creation + parser.add_argument("--new-case", metavar="CASE_NUMBER", help="Create new case") + parser.add_argument("--name", metavar="NAME", help="Name for new case") + parser.add_argument("--investigator", metavar="INVESTIGATOR", help="Investigator name for new case") + parser.add_argument("--new-evidence", metavar="EVIDENCE_NAME", help="Create new evidence in active case") + parser.add_argument("--description", metavar="DESC", help="Description for new evidence") + + # Export + parser.add_argument("--export", action="store_true", help="Export all data to Markdown file") + parser.add_argument("--output", metavar="FILE", default="trace_export.md", help="Output file for export") + + # TUI + parser.add_argument("--open", "-o", action="store_true", help="Open TUI directly at active case/evidence") args = parser.parse_args() + # Handle context management commands + if args.show_context: + show_context() + return + + if args.list: + list_contexts() + return + + if args.switch_case: + switch_case(args.switch_case) + return + + if args.switch_evidence: + switch_evidence(args.switch_evidence) + return + + # Handle case/evidence creation + if args.new_case: + create_case(args.new_case, name=args.name, investigator=args.investigator) + return + + if args.new_evidence: + create_evidence(args.new_evidence, description=args.description) + return + + # Handle export if args.export: export_markdown(args.output) return - if args.note: - quick_add_note(args.note) + # Handle note addition + if args.stdin: + # Read from stdin + content = sys.stdin.read().strip() + if not content: + print("Error: No content provided from stdin.", file=sys.stderr) + sys.exit(1) + quick_add_note(content, case_override=args.case, evidence_override=args.evidence) return - # Check for first run and run GPG wizard if needed + if args.note: + quick_add_note(args.note, case_override=args.case, evidence_override=args.evidence) + return + + # No arguments - check for first run and launch TUI from .gpg_wizard import check_and_run_wizard check_and_run_wizard() From 33cad5bd5f71df6e60bf3cc0435ae01e450f71ec Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Dec 2025 13:17:42 +0000 Subject: [PATCH 2/2] Document new CLI commands in README Added comprehensive CLI Command Reference section documenting: - Context management commands (--show-context, --list, --switch-*) - Case/evidence creation commands (--new-case, --new-evidence) - Advanced note-taking features (--stdin, --case, --evidence overrides) - Export commands Placed section prominently after the hot logging feature overview to maintain focus on the primary use case while documenting the full CLI surface area. --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index 3d8c2de..e8da9a5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,76 @@ trace "Observed outbound connection to 192.168.1.55 on port 80. #suspicious #net **System Integrity Chain:** Each command-line note is immediately stamped, concatenated with its content, and hashed using SHA256 before storage. This ensures a non-repudiable log entry. +## CLI Command Reference + +### Context Management + +View and switch between cases and evidence without opening the TUI: + +```bash +# Show current active case and evidence +trace --show-context + +# List all cases and evidence in hierarchy +trace --list + +# Switch active case (by case number or UUID) +trace --switch-case 2024-001 + +# Switch active evidence (by name or UUID, within active case) +trace --switch-evidence "disk-image-1" +``` + +### Case and Evidence Creation + +Create new cases and evidence directly from the command line: + +```bash +# Create new case (automatically becomes active) +trace --new-case 2024-001 + +# Create case with full metadata +trace --new-case 2024-001 --name "Ransomware Investigation" --investigator "Jane Doe" + +# Create evidence in active case (automatically becomes active) +trace --new-evidence "Laptop HDD" + +# Create evidence with description +trace --new-evidence "Server Logs" --description "Apache logs from compromised server" +``` + +### Advanced Note-Taking + +Beyond basic hot logging, trace supports stdin piping and context overrides: + +```bash +# Pipe command output directly into notes +ps aux | grep malware | trace --stdin +tail -f /var/log/auth.log | grep "Failed password" | trace --stdin +netstat -an | trace --stdin + +# Add note to specific case without changing active context +trace --case 2024-002 "Found malware in temp folder" + +# Add note to specific evidence without changing active context +trace --evidence "Memory Dump" "Suspicious process identified" + +# Override both case and evidence for a single note +trace --case 2024-001 --evidence "Disk Image" "Recovered deleted files" +``` + +**Identifiers:** All commands accept both human-friendly identifiers (case numbers like `2024-001`, evidence names like `Laptop HDD`) and UUIDs. Use `--list` to see available identifiers. + +### Export + +```bash +# Export all data to markdown (GPG-signed if enabled) +trace --export --output investigation-report.md + +# Export with default filename (trace_export.md) +trace --export +``` + ## Installation & Deployment ### Quick Install from Latest Release