mirror of
https://github.com/overcuriousity/trace.git
synced 2025-12-20 04:52:21 +00:00
Add GPG signing for entire markdown exports
When exporting to markdown (--export), the entire export document is now
signed with GPG if signing is enabled in settings.
Features:
- Builds export content in memory before signing
- Signs the complete document as one GPG clearsigned block
- Individual note signatures are preserved within the export
- Provides two layers of verification:
1. Document-level: Verifies entire export hasn't been modified
2. Note-level: Verifies individual notes haven't been tampered with
Verification workflow:
- Entire export: gpg --verify export.md
- Individual notes: Extract signature blocks and verify separately
Changes:
- Renamed write_note() to format_note_for_export() returning string
- Export content built in memory before file write
- Signs complete export if pgp_enabled=True
- Shows verification instructions after successful export
Example output:
✓ Export signed with GPG
✓ Exported to case-2024-001.md
To verify the export:
gpg --verify case-2024-001.md
This commit is contained in:
134
trace/cli.py
134
trace/cli.py
@@ -79,68 +79,106 @@ def quick_add_note(content: str):
|
||||
def export_markdown(output_file: str = "export.md"):
|
||||
try:
|
||||
storage = Storage()
|
||||
state_manager = StateManager()
|
||||
settings = state_manager.get_settings()
|
||||
|
||||
# Build the export content in memory first
|
||||
content_lines = []
|
||||
content_lines.append("# Forensic Notes Export\n\n")
|
||||
content_lines.append(f"Generated on: {time.ctime()}\n\n")
|
||||
|
||||
for case in storage.cases:
|
||||
content_lines.append(f"## Case: {case.case_number}\n")
|
||||
if case.name:
|
||||
content_lines.append(f"**Name:** {case.name}\n")
|
||||
if case.investigator:
|
||||
content_lines.append(f"**Investigator:** {case.investigator}\n")
|
||||
content_lines.append(f"**Case ID:** {case.case_id}\n\n")
|
||||
|
||||
content_lines.append("### Case Notes\n")
|
||||
if not case.notes:
|
||||
content_lines.append("_No notes._\n")
|
||||
for note in case.notes:
|
||||
note_content = format_note_for_export(note)
|
||||
content_lines.append(note_content)
|
||||
|
||||
content_lines.append("\n### Evidence\n")
|
||||
if not case.evidence:
|
||||
content_lines.append("_No evidence._\n")
|
||||
|
||||
for ev in case.evidence:
|
||||
content_lines.append(f"#### Evidence: {ev.name}\n")
|
||||
if ev.description:
|
||||
content_lines.append(f"_{ev.description}_\n")
|
||||
content_lines.append(f"**ID:** {ev.evidence_id}\n")
|
||||
|
||||
# Include source hash if available
|
||||
source_hash = ev.metadata.get("source_hash")
|
||||
if source_hash:
|
||||
content_lines.append(f"**Source Hash:** `{source_hash}`\n")
|
||||
content_lines.append("\n")
|
||||
|
||||
content_lines.append("##### Evidence Notes\n")
|
||||
if not ev.notes:
|
||||
content_lines.append("_No notes._\n")
|
||||
for note in ev.notes:
|
||||
note_content = format_note_for_export(note)
|
||||
content_lines.append(note_content)
|
||||
content_lines.append("\n")
|
||||
content_lines.append("---\n\n")
|
||||
|
||||
# Join all content
|
||||
export_content = "".join(content_lines)
|
||||
|
||||
# Sign the entire export if GPG is enabled
|
||||
if settings.get("pgp_enabled", False):
|
||||
gpg_key_id = settings.get("gpg_key_id", None)
|
||||
signed_export = Crypto.sign_content(export_content, key_id=gpg_key_id)
|
||||
|
||||
if signed_export:
|
||||
# Write the signed version
|
||||
final_content = signed_export
|
||||
print(f"✓ Export signed with GPG")
|
||||
else:
|
||||
# Signing failed - write unsigned
|
||||
final_content = export_content
|
||||
print("⚠ Warning: GPG signing failed. Export saved unsigned.", file=sys.stderr)
|
||||
else:
|
||||
final_content = export_content
|
||||
|
||||
# Write to file
|
||||
with open(output_file, "w", encoding='utf-8') as f:
|
||||
f.write("# Forensic Notes Export\n\n")
|
||||
f.write(f"Generated on: {time.ctime()}\n\n")
|
||||
f.write(final_content)
|
||||
|
||||
for case in storage.cases:
|
||||
f.write(f"## Case: {case.case_number}\n")
|
||||
if case.name:
|
||||
f.write(f"**Name:** {case.name}\n")
|
||||
if case.investigator:
|
||||
f.write(f"**Investigator:** {case.investigator}\n")
|
||||
f.write(f"**Case ID:** {case.case_id}\n\n")
|
||||
print(f"✓ Exported to {output_file}")
|
||||
|
||||
f.write("### Case Notes\n")
|
||||
if not case.notes:
|
||||
f.write("_No notes._\n")
|
||||
for note in case.notes:
|
||||
write_note(f, note)
|
||||
# Show verification instructions
|
||||
if settings.get("pgp_enabled", False) and signed_export:
|
||||
print(f"\nTo verify the export:")
|
||||
print(f" gpg --verify {output_file}")
|
||||
|
||||
f.write("\n### Evidence\n")
|
||||
if not case.evidence:
|
||||
f.write("_No evidence._\n")
|
||||
|
||||
for ev in case.evidence:
|
||||
f.write(f"#### Evidence: {ev.name}\n")
|
||||
if ev.description:
|
||||
f.write(f"_{ev.description}_\n")
|
||||
f.write(f"**ID:** {ev.evidence_id}\n")
|
||||
|
||||
# Include source hash if available
|
||||
source_hash = ev.metadata.get("source_hash")
|
||||
if source_hash:
|
||||
f.write(f"**Source Hash:** `{source_hash}`\n")
|
||||
f.write("\n")
|
||||
|
||||
f.write("##### Evidence Notes\n")
|
||||
if not ev.notes:
|
||||
f.write("_No notes._\n")
|
||||
for note in ev.notes:
|
||||
write_note(f, note)
|
||||
f.write("\n")
|
||||
f.write("---\n\n")
|
||||
print(f"Exported to {output_file}")
|
||||
except (IOError, OSError, PermissionError) as e:
|
||||
print(f"Error: Failed to export to {output_file}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def write_note(f, note: Note):
|
||||
f.write(f"- **{time.ctime(note.timestamp)}**\n")
|
||||
f.write(f" - Content:\n")
|
||||
def format_note_for_export(note: Note) -> str:
|
||||
"""Format a single note for export (returns string instead of writing to file)"""
|
||||
lines = []
|
||||
lines.append(f"- **{time.ctime(note.timestamp)}**\n")
|
||||
lines.append(f" - Content:\n")
|
||||
# Properly indent multi-line content
|
||||
for line in note.content.splitlines():
|
||||
f.write(f" {line}\n")
|
||||
f.write(f" - Hash: `{note.content_hash}`\n")
|
||||
lines.append(f" {line}\n")
|
||||
lines.append(f" - Hash: `{note.content_hash}`\n")
|
||||
if note.signature:
|
||||
f.write(" - **Signature Verified:**\n")
|
||||
f.write(" ```\n")
|
||||
lines.append(" - **Individual Note Signature:**\n")
|
||||
lines.append(" ```\n")
|
||||
# Indent signature for markdown block
|
||||
for line in note.signature.splitlines():
|
||||
f.write(f" {line}\n")
|
||||
f.write(" ```\n")
|
||||
f.write("\n")
|
||||
lines.append(f" {line}\n")
|
||||
lines.append(" ```\n")
|
||||
lines.append("\n")
|
||||
return "".join(lines)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="trace: Forensic Note Taking Tool")
|
||||
|
||||
Reference in New Issue
Block a user