Skip to content

Add tsk_notes for comments, AI answers and summaries - #41

Closed
jayaramcs wants to merge 1 commit into
develop-4.1xfrom
tsk_notes
Closed

Add tsk_notes for comments, AI answers and summaries#41
jayaramcs wants to merge 1 commit into
develop-4.1xfrom
tsk_notes

Conversation

@jayaramcs

Copy link
Copy Markdown
Member

Implements the "Notes in The Sleuth Kit" design proposal (revision 2).

One new table for text that belongs to an object in the case, has no score, and can change after it is written. Comments, AI enrichment, remediation advice and host or case summaries all come from it. Analysis results stay immutable, which is why this is a new table rather than more columns on an existing one.

Schema 9.9 (from 9.8)

  • tsk_note_types — open types, seeded with the built-ins on every case open, extended by consumers at runtime.
  • tsk_notes — append-only. An edit inserts a new row sharing the same original_note_id and clears is_current on the row it replaces, so earlier text survives and outside references stay correct.
  • Indexes per the proposal, including tsk_notes_current_revision_index, a partial UNIQUE on (original_note_id) WHERE is_current = 1. It is on both engines: SQLite has supported partial indexes since 3.8.0, and this is what settles the revision flip rather than leaving it to every call site.
  • updateFromSchema9dot8toSchema9dot9(), modelled on the 9.7-to-9.8 method.

The CASE object

ObjectType.CASE(10), one per case database, par_obj_id NULL, its id recorded in tsk_db_info_extended under CASE_OBJECT_ID. It is a sibling of the data sources, not their parent.

A new object type is deliberately more backward compatible here than reusing ARTIFACT: ObjectType.valueOf(short) returns UNSUPPORTED for unknown values, getRootObjects() skips UNSUPPORTED, and getContentById() falls through to UnsupportedContent. All of that shipped in 9.0, and isCompatible() checks the major version only, so every build that can open a 9.x database has the escape hatch. A root-level artifact would throw in every build from 9.0 to 9.8.

case CASE: break; in getRootObjects() is not optional — once the enum knows the value it stops mapping to UNSUPPORTED and would hit the throwing default:.

No Case class implementing Content: that would add a method to the public ContentVisitor and SleuthkitItemVisitor interfaces and force a coordinated release with every implementor. It is exposed as a plain long from SleuthkitCase.getCaseObjectId().

API

NoteManager via skCase.getNoteManager(), following HostManager. Reads do not filter — superseded revisions and soft-deleted rows come back with everything else and the caller decides what to render — with getCurrentNotes(), getCurrentRevision() and getCurrentNoteCounts() as the opt-in narrow reads.

TSK_NOTE_ID(160, LONG) lets an analysis result point back at the note holding its reasoning. The value is original_note_id, never a revision id, so the attribute is written once and stays correct across edits.

Judgement calls worth a reviewer's attention

  1. addNote() delegates to addNotes(). The proposal asks for batch/single parity on the derived columns; making them one code path is stronger than testing for it.
  2. PG_NOTES_CHUNK_SIZE = 4000, not PG_CHUNK_SIZE = 9000. 13 bound columns x 9000 = 117k parameters, over PostgreSQL's 65,535 ceiling. Sized the way PG_FILES_CHUNK_SIZE is.
  3. Hard delete takes the whole revision lineage, not just the named row. Deleting only the first version throws a foreign key violation, since later revisions reference it — the cascade through parent_note_id alone is not enough. Soft delete works on the lineage too, so a consumer holding the stable original_note_id retracts the note a reader can see rather than a superseded draft.
  4. Added getCurrentNoteCounts() beside getNoteCounts(). The broad-read contract means getNoteCounts() counts every revision, which is the wrong number for the badge it exists for. It counts exactly what getCurrentNotes() returns, retracted notes included — whether a retraction is shown is a product ruling, and it has to be the same ruling for the badge and the list behind it.
  5. The case object is created in one place, the get-or-create in SleuthkitCase.init(), rather than also in CaseDatabaseFactory and the upgrade method. newCase() always opens the case and the upgrade runs inside init(), so all three entry points in the proposal are covered by one idempotent implementation. It is race-safe on the primary key of tsk_db_info_extended.name, since acquireSingleUserCaseWriteLock() is a JVM lock and does nothing across PostgreSQL clients.

Testing

NoteTest, added to DataModelTestSuite. 67 tests pass, no regressions in the existing suite.

Covers each item on the proposal's test list: cascade of a deleted thread (including one whose reply was revised), the unique index rejecting a second current revision rather than the manager racing, batch-versus-single parity on root_note_id and original_note_id, case-object get-or-create on a fresh and an upgraded case, and reviseNote() rejecting a different author.

upgradeFromSchema9dot8Test takes a 9.9 case back apart to 9.8 and reopens it, so the upgrade method is exercised rather than assumed.

The multi-row INSERT only runs on PostgreSQL in production and would otherwise have no coverage. Its SQL is engine-neutral, so there is a package-private addNotes(requests, trans, batched) seam and a test that runs it against SQLite and checks it against the row-at-a-time path.

Not verified

  • Anything genuinely PostgreSQL-specific: ON CONFLICT DO NOTHING, the partial indexes, and the batched insert under reWriteBatchedInserts=true. No PostgreSQL server was available.
  • The compatibility check the proposal calls for: building a 9.9 database with a CASE row and opening it in a shipped Autopsy 9.x. The argument rests on the UNSUPPORTED path and was checked by reading this repository, but it is still an argument about how other software behaves.

Left alone deliberately

  • tsk/auto/db_sqlite.cpp holds a stale copy of the schema and only tsk_loaddb uses that path; both real consumers go through SleuthkitCase.newCase().
  • bindings/java/doxygen/schema/ is stale at 9.4 and the last four schema bumps did not touch it.

Open product decisions (unchanged by this PR)

Whether the delete button does a hard or soft delete, whether reports and exports show retracted or superseded notes, chat policy, and whether Autopsy surfaces notes in this release. The schema and API answer either way.

🤖 Generated with Claude Code

- Add tsk_notes and tsk_note_types at schema 9.9, for text that belongs to an
  object in the case, has no score, and can change after it is written
- Add a root-level CASE object so case-wide notes have something to point at,
  with its id recorded in tsk_db_info_extended
- Add NoteManager, Note, NoteType and NoteRequest, with batch reads and writes
  and append-only revisions rather than in-place edits
- Add the TSK_NOTE_ID attribute so an analysis result can point back at the
  note holding its reasoning
- Add note added, updated and deleted events, fired once per batch after commit
- Add NoteTest covering threading, revisions, deletes, batch parity and the
  9.8 to 9.9 upgrade
@jayaramcs jayaramcs closed this Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant