Reading width
Wide uses the full column for everything, text, diagrams, code, and exercises. Narrow keeps the standard reading width.
Text size
Scales the body text. Headings and code blocks keep their size.
In this section
The macOS Forensic Toolstack: mac_apt, APOLLO, ccl-segb, and the Native Commands
Introduction
A macOS collection is several different kinds of thing wearing one directory structure, and the format of each store decides which tools can read it at all.
That makes tool selection a reading problem rather than a matter of preference. Some stores have several parsers and the choice is genuinely yours; some have one; and a few have one that nobody has updated since the last release changed the format underneath it.
This section is about matching the tool to the format, and about the specific point where a broad pass stops being evidence and becomes a map.
Scenario
A breadth pass over the collection from NE-VANCE-MBP completes in an afternoon and produces a database of parsed artifacts. The report is written from that database. Nobody opens a raw store, and nobody records which release of the tool produced which table.
The format decides the tool
Which is the first move on every artifactStart with what a collection actually contains. Property lists hold configuration and persistence in a binary encoding you convert before reading. Databases back the behavioral stores, the browsers and the privacy grants, and you query them directly. Unified logging writes a compressed trace that Apple's own tooling reads cleanly and little else does. The newer activity streams use a record format that a single parser handles.
FOUR FORMATS IN ONE COLLECTION
property list configuration, persistence convert, then read
database behavior, browsers, privacy query it directly
trace store the unified log Apple tooling, or one parser
record stream the newer activity streams a single parser reads it
the format is the thing you read off the file first
Four formats and four different doors. Reading the format off a file before choosing a tool is the move that prevents most of the wasted effort on this platform, and it takes seconds compared with the hour spent discovering that a suite cannot open something.
The analysis stack here is almost entirely open source, and that is deliberate rather than a budget decision. You can read the code, so when a parser claims an artifact says something you can confirm the claim against the source rather than trusting a result you cannot inspect. In a discipline where the output ends up in a report somebody else will challenge, being able to say why a tool produced a value is worth more than convenience.
There is a practical corollary about mixed stacks. Commercial suites still earn their place in acquisition, where the engineering is genuinely hard and the licensing buys tested code against hardware you cannot replicate. The reading, though, is where inspectability pays, and there is no contradiction in acquiring with one and analyzing with the other. What matters is knowing which half of the work you are in when a result surprises you.
Reading the format off a file is also cheaper than it sounds. The first bytes of most of these stores identify them, the extension is usually honest, and where neither settles it the platform's own conversion tool will either open the file or refuse it in a way that tells you what it is not. Thirty seconds spent there routinely saves an hour spent asking why a suite produced an empty table. The habit generalizes: on a platform that changes stores annually, identifying what you are holding is the one step that never goes out of date.
Start with breadth
And know what it costsThe first pass over a collection is breadth, and the workhorse is a plugin framework rather than a suite. It takes a disk image, a mounted volume or a targeted artifact set, runs modular parsers across all of it, and writes one table per plugin into a single database. You point it at the collection and let it map the landscape before committing to a theory about what happened.
Reading across the resulting tables is how you find the store your question points at without opening every file by hand. Each plugin name is a category of evidence rather than a tool feature: one reads download provenance, one parses the trace store, one turns the filesystem changelog into rows, one recovers shell history for every user on the machine.
Decide
What an empty table supportsThe breadth pass over the collection completes without errors. Every plugin reports finished. The table for the newer activity stream contains no rows at all, and the machine is running the current release. Nobody recorded which version of the tool was run.
What does the empty table establish?
The decision above is the one the scenario's report got wrong. Commit to an answer before reading the reasoning underneath it, because an examiner who commits first learns considerably more from the explanation than one who scrolls straight past it.
The framework runs against more than a mounted volume, which is worth knowing before you convert anything. It accepts disk images in the common formats, targeted artifact sets, and collections produced by other triage tools, so a set of files somebody else pulled from a live machine can go through the same pass as a full image. That removes an argument about whether the collection was the right shape before analysis can begin.
It also means the breadth pass is available on evidence you did not collect yourself, which is the common case in a handover. Somebody else's triage output can be mapped the same way, and the resulting tables will tell you what that collection actually contained rather than what its operator believed they had taken. That gap between what was collected and what somebody thinks was collected is common enough in handovers to be worth checking rather than assuming.
What the breadth pass costs
In time and in diskRun carelessly, that first pass takes an afternoon and produces something unwieldy, and two facts about it save most of that time. The default output is a database, while the optional delimited exports are encoded in a way that defeats naive command-line parsing and opens cleanly in a spreadsheet instead. Knowing which you want before the run decides how you will read the result.
# The whole map, when you do not yet know the question
% python mac_apt.py MOUNTED /Volumes/NE-VANCE-MBP /cases/vance/macapt ALL
# Named plugins, when you do: minutes instead of an afternoon
% python mac_apt.py MOUNTED /Volumes/NE-VANCE-MBP /cases/vance/macapt \
QUARANTINE FSEVENTS TERMSESSIONS
The second form is the one to reach for once a question exists. The unified-log plugin in particular is the heavy one in the set: a forty-gigabyte image can produce a log database over ten gigabytes on its own, which is a meaningful share of an afternoon and of a case drive.
There is a sequencing argument buried in that. The full pass is right when you do not yet know what you are looking for, because its value is the map. Once a question exists, named plugins finish in minutes and produce something small enough to read, and running the full set again out of habit costs hours for tables nobody will open.
The output format decision deserves the same thought. A database is queryable, joinable and awkward to skim; the delimited export is readable in a spreadsheet and awkward to query. Investigations that end in a timeline usually want the first, and a question answered by sorting one table usually wants the second. Choosing after the run means converting, which is a step that exists only because nobody chose before it.
One detail about the delimited exports catches people once. The encoding they use defeats naive command-line parsing, so a pipeline built around them will produce nonsense rather than an error, and the file will open perfectly in a spreadsheet at the same time. An examiner who sees garbage from a script and clean columns in an application is looking at an encoding mismatch rather than a corrupted export. Knowing that saves the re-run, which on a large image is most of an afternoon.
Where the pass goes stale
Which is the scenario's errorAll of which assumes the tool read everything it was pointed at, and on this platform that assumption has a shelf life. A broad processor is fast precisely because it makes assumptions about formats, the platform reorganizes its stores every year, and the tool predates the newest of them. The pass is a map rather than the territory.
Four things the pass gives you, and the four it does not.
Read the dashed arrows as the half of the picture the output never mentions, which is what makes a complete-looking result the one to distrust.
A triage pass output, read as evidence
Complete-lookingHow to read it without over-reading it
- As a map of where the evidence for your question lives, it is fast and it is the right first move.
- A store whose format changed in this year's release can be absent from the output with no error and no note.
- When the case turns on a single row, the raw record behind it is what you open, and the pass does not carry it.
The output that should worry you is the one that looks complete, because completeness is the one property this artifact cannot demonstrate about itself.
Read the rows across rather than down, because both columns describe the same run. The fourth row is the one that catches people: a parser that cannot read a store does not announce the gap, so an output that looks complete is not the same as a run that read everything.
So the rule is narrow and worth memorizing. When the output looks wrong, when the case turns on a single record, or when the machine runs a release the tool predates, open the raw store and confirm it yourself. The pass tells you where to look, and it does not get to tell you what you found.
The same rule has a second edge worth stating. A parsed row that does answer your question is still a claim made by software about a file, and where the case turns on that single row the raw store is where the claim gets confirmed. That is not distrust of the tool; it is the difference between reporting what a parser output and reporting what the evidence says, and only one of those survives somebody else running a different parser.
In practice the confirmation is quick where it matters. Open the store the row came from, find the record, and satisfy yourself that the parsed value is what the record says. For a single decisive row that is minutes, and it converts a finding that rests on tooling into one that rests on evidence.
Opening a store yourself
And the one it is worth doing onOpening a store yourself is a smaller step than it sounds, and the privacy database is the one worth learning it on. It answers a question the Windows platform never asks: which applications hold access to the camera, the microphone, the whole filesystem, or the interface that can read keystrokes from every other application.
# Read the privacy-grant store straight off the mounted image
% cd "/Volumes/NE-VANCE-MBP/Library/Application Support/com.apple.TCC"
% sqlite3 TCC.db \
"SELECT service, client, auth_value,
datetime(last_modified,'unixepoch') FROM access"
Read the result like an analyst rather than a list. A grant value meaning enforced tells you the permission was live, and the client column tells you whether the holder is named by bundle identifier or by an absolute path to an executable, which is itself a signal worth noticing.
The modification timestamp is the forensic value. It records when each grant changed, which drops straight onto a timeline, and a grant written minutes before something ran is a different fact from one written a year earlier. There is a system database and one per user, so establishing which you are reading is part of stating the finding rather than a detail.
On a live machine both of those databases are protected and a tool needs explicit access to open them, which is the same constraint the module has met elsewhere. On a mounted image that protection does not apply, because it belonged to the running system rather than to the volume, so reading them from the image is ordinary work. Knowing that in advance stops an examiner weakening their own machine to solve a problem that only exists on a live subject.
The per-user and system split is worth reading as a question about scope rather than a filing detail. A grant in a user's own database speaks to what that account permitted; a grant in the system database applies more broadly. Reporting one as the other overstates or understates the reach of a permission, and the two files sit in different places precisely because they answer different questions. Stating which you read, alongside the grant itself, is what keeps the claim at the width the evidence supports.
Stores with exactly one parser
Where the choice disappearsFor some stores the question of which tool answers itself, and recognizing that is part of knowing the stack. The behavioral and activity stores that place a person at the keyboard have a standard parser and effectively no alternative, and the newer record-stream format defeats the mainstream suites today.
Three tiers of choice, and the bottom one is not a choice.
Read the block as three tiers of choice, and notice that only the top tier is a preference. The middle and bottom are constraints, and constraints belong in the record rather than in somebody's head.
THREE TIERS, AND WHAT EACH ONE ASKS OF YOU
many parsers choose on convenience, confirm against the raw file
few parsers the native tooling settles any disagreement
one parser no choice, and its maintenance is your capability
the bottom tier is the one to state in a report
The bottom row deserves a moment. Where a single project is the only thing that reads a store, that project's maintenance schedule is part of your evidential capability, which is an uncomfortable dependency and one worth stating in a report rather than discovering mid-case. It is also an argument for knowing which of your tools sit in that tier before you need them.
The corollary for planning is unglamorous and cheap. Once a year, or after any release that moves a store, check that the single-parser tools in your stack still read the current format end to end, and note the version that worked. A tier-three dependency discovered mid-case is a problem; the same dependency checked in advance is a known constraint you can plan a collection around.
The same check has a reporting use. If you can say which version of a single-parser tool read a store and when that version was confirmed against the current release, the dependency stops being a weakness in the examination and becomes a documented condition of it. Unstated, it is the thing an opposing examiner raises; stated, it is something you already accounted for. The difference between those two positions costs a single line in the record. It is the cheapest insurance in the toolstack, and the only one that has to be bought before the risk is visible.
The native toolchain is the arbiter
When two tools disagreeWhen two parsers produce different answers from the same file, something has to be the reference. The operating system's own tooling reads its own formats definitively, which makes it the arbiter rather than merely another option in the comparison.
# Convert a binary property list to readable form without altering it
% plutil -p /Volumes/NE-VANCE-MBP/Library/LaunchDaemons/com.example.helper.plist
# Read a collected log archive with the platform's own reader
% log show --archive /cases/vance/logs.logarchive --style compact --last 6h
Read the commands as conversion and as reading, both done by the platform itself, and neither altering the store it touches. That property is what lets you use them freely during an examination without adding a change you would have to explain.
There is one more reason to reach for the native tooling first rather than last. It is present on every machine running the operating system, needs no installation, carries no download attribute and requires no signature decision, so it is the one part of the stack that works on a borrowed machine or in an environment where installing software is not an option. That makes it the fallback as well as the arbiter. Both roles come from the same property, which is that it ships with the system rather than alongside it.
The same properties make it the right thing to reach for in the first hour of a live response, where installing anything is a change to the machine and a decision to justify. Working with what is already present keeps the intervention small, and small interventions are easier to describe afterwards than a toolkit somebody deployed under pressure.
There is a practical limit on the graphical side worth knowing before you rely on it. The console application gives comfortable access to the logs, and its output cannot be piped into command-line tools or scripts, which rules it out for anything you need to repeat or evidence. So the native tooling is where you confirm and the open-source stack is where you work at speed, and a report that never touched the first is one where nothing was checked against the format itself.
Recording which tool read what
Because versions decide findingsRecording the tool against each store is the step the scenario skipped entirely, and it costs seconds. On a platform that changes formats annually the version is part of the finding, because a table produced by a release that predates the store it claims to have read is not evidence of absence.
THE TOOL LINE, PER STORE
the store which artifact was read
the tool its name, and the version that produced the table
confirmed by the raw store, or the native tooling, or neither
gaps stores the tool could not parse, named explicitly
the last line is the one nobody writes
Read the block as the tool line a report carries per store. The confirmation line records whether anybody checked a row against the raw store, and the last line is the one nobody writes.
Naming the gaps is what separates a parsed collection from an examined one. A store the tool silently skipped is indistinguishable in the output from a store that held nothing, and only the person who ran it can tell the reader which it was. Writing that down while it is fresh is trivial, and reconstructing it afterwards is guesswork presented with the authority of a record.
The version matters more here than in most disciplines because of how quickly the platform moves. A parser that was current when a case opened may be two releases behind by the time the report is challenged, and the question will be what it could read at the time rather than what it can read now. Recording the version answers that question without anybody having to reconstruct which build was installed on which afternoon.
It is also the only practical way to scope the damage if a parser is later found to mishandle a format. With versions recorded per store per case, the affected work is a query; without them, it is a review of everything, which in practice means nobody does it and the question stays open. An open question of that kind does not resolve itself; it simply sits with the case until somebody else asks it.
Practice
Read one store two ways hands onReading the same store through a parser and through the native tooling is the habit that makes the rest of this section concrete.
- Answer the scenario. Name what the report is entitled to say from a breadth pass alone, and what it is not.
- Sort the formats. List the four and say which of them leaves you a genuine choice of tool.
- State the stale case. Say what a breadth tool does when it meets a store whose format changed, and why that is dangerous in an output that looks complete.
- Now on a Mac you have permission to use, convert a binary property list with the native tool and read it.
- Then read the privacy database directly and look at which clients are named by path rather than by identifier. This is the reading that the breadth pass would have flattened into a row.
The format of a store decides which tools can read it, so reading the format off a file precedes choosing a tool. A breadth pass across a collection maps where the evidence for a question lives, and its output is a map rather than the territory: a broad processor makes format assumptions, the platform reorganizes its stores annually, and a store the tool cannot parse is silently absent from a result that still looks complete. Named plugins finish in minutes where a full pass takes an afternoon, and the unified-log plugin alone can turn a forty-gigabyte image into a log database over ten gigabytes. Some stores have exactly one parser, which makes that project's maintenance part of your evidential capability, and the platform's own tooling is what a disagreement between parsers is measured against.