From 7cca4b514e95ae9070f8e982607dda9b83352f53 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 21 May 2024 00:52:50 -0400 Subject: [PATCH 1/8] ese updates --- .../docs/Artifacts/Windows Artfacts/bits.md | 6 +- .../docs/Artifacts/Windows Artfacts/ual.md | 55 ++- .../Artifacts/Windows Artfacts/updates.md | 17 +- src/windows/ese.ts | 352 ++++++++++++++++- src/windows/ese/ual.ts | 374 +++++++++++------- src/windows/ese/updates.ts | 326 ++++++++------- types/windows/bits.ts | 8 +- types/windows/ese.ts | 122 +++++- types/windows/ese/updates.ts | 1 + 9 files changed, 926 insertions(+), 335 deletions(-) diff --git a/artemis-docs/docs/Artifacts/Windows Artfacts/bits.md b/artemis-docs/docs/Artifacts/Windows Artfacts/bits.md index 64523dcd..3369975d 100644 --- a/artemis-docs/docs/Artifacts/Windows Artfacts/bits.md +++ b/artemis-docs/docs/Artifacts/Windows Artfacts/bits.md @@ -25,7 +25,7 @@ Other Parsers: References: - [BitsAdmin](https://ss64.com/nt/bitsadmin.html) -- [Background Intelligrent Transfer Service](https://en.wikipedia.org/wiki/Background_Intelligent_Transfer_Service) +- [Background Intelligent Transfer Service](https://en.wikipedia.org/wiki/Background_Intelligent_Transfer_Service) - [BITS](https://www.mandiant.com/resources/blog/attacker-use-of-windows-background-intelligent-transfer-service) # TOML Collection @@ -200,8 +200,8 @@ export interface Files { /**Number of bytes downloaded */ download_bytes_size: number; /**Number of bytes transferred */ - trasfer_bytes_size: number; - /**Fulll file path associated with Job */ + transfer_bytes_size: number; + /**Full file path associated with Job */ full_path: string; /**Filename associated with Job */ filename: string; diff --git a/artemis-docs/docs/Artifacts/Windows Artfacts/ual.md b/artemis-docs/docs/Artifacts/Windows Artfacts/ual.md index 873181e8..a1b35431 100644 --- a/artemis-docs/docs/Artifacts/Windows Artfacts/ual.md +++ b/artemis-docs/docs/Artifacts/Windows Artfacts/ual.md @@ -11,12 +11,6 @@ The Windows User Access Log (UAL) is an ESE database containing logon activity to a system. This database only exists on Windows Servers. Artemis supports parsing both unlocked and locked UAL databases. -By default artemis will try to parse the database files at: - -- System32\\LogFiles\\Sum - -However, you may provide an optional alternative path to the UAL databases. - This database is **not** related to to the M365 UAL (Unified Audit Logging). # Collection @@ -27,15 +21,52 @@ keys. # Sample API Script ```typescript -import { - userAccessLog, -} from "https://raw.githubusercontent.com/puffycid/artemis-api/master/mod.ts"; +import { FileError } from "../../Projects/artemis-api/src/filesystem/errors.ts"; +import { glob } from "../../Projects/artemis-api/src/filesystem/files.ts"; +import { WindowsError } from "../../Projects/artemis-api/src/windows/errors.ts"; +import { UserAccessLogging } from "../../Projects/artemis-api/src/windows/ese/ual.ts"; + +function main() { + const glob_path = "C:\\System32\\LogFiles\\Sum\\*.mdb"; + const paths = glob(glob_path); + if (paths instanceof FileError) { + return; + } + + let role = undefined; + for (const path of paths) { + if (path.filename != "SystemIdentity.mdb") { + continue; + } -async function main() { - const results = userAccessLog(); + const ual = new UserAccessLogging(path.full_path); + role = ual; + } - console.log(results); + if(role === undefined) { + return; + } + + console.log(role.pages); + + for (const path of paths) { + if (path.filename === "SystemIdentity.mdb") { + continue; + } + console.log(`Parsing: ${path.full_path}`); + + const clients = new UserAccessLogging(path.full_path); + + const data = clients.getUserAccessLog(clients.pages, role); + if (data instanceof WindowsError) { + console.error(data); + continue; + } + console.log(data.length); + } } + +main(); ``` # Output Structure diff --git a/artemis-docs/docs/Artifacts/Windows Artfacts/updates.md b/artemis-docs/docs/Artifacts/Windows Artfacts/updates.md index b354ea80..99a635a2 100644 --- a/artemis-docs/docs/Artifacts/Windows Artfacts/updates.md +++ b/artemis-docs/docs/Artifacts/Windows Artfacts/updates.md @@ -10,7 +10,7 @@ keywords: Artemis supports parsing the history of Windows updates on the system. By default artemis will try to parse the ESE database at: -- Windows\SoftwareDistribution\DataStore.edb +- Windows\SoftwareDistribution\DataStore\DataStore.edb You may also provide an alternative path to DataStore.edb. @@ -22,15 +22,15 @@ Windows Updates. # Sample API Script ```typescript -import { - updateHistory, -} from "https://raw.githubusercontent.com/puffycid/artemis-api/master/mod.ts"; +import { Updates } from "../../Projects/artemis-api/src/windows/ese/updates.ts"; -async function main() { - const results = updateHistory(); - - console.log(results); +function main() { + const updates = new Updates(); + console.log(updates.updateHistory(updates.pages)); } + +main(); + ``` # Output Structure @@ -41,6 +41,7 @@ An array of `UpdateHistory` export interface UpdateHistory { client_id: string; support_url: string; + /**Timestamp in UNIXEPOCH seconds */ date: number; description: string; operation: Operation; diff --git a/src/windows/ese.ts b/src/windows/ese.ts index 49927b9a..9e94ad37 100644 --- a/src/windows/ese.ts +++ b/src/windows/ese.ts @@ -1,23 +1,337 @@ -import { EseTable } from "../../types/windows/ese.ts"; +import { + Catalog, + CatalogType, + ColumnFlags, + ColumnInfo, + ColumnType, + EseTable, + TableInfo, +} from "../../types/windows/ese.ts"; import { WindowsError } from "./errors.ts"; -/** - * Function to parse any ESE database and tables - * @param path Path to ESE database - * @param tables Tables that should be parsed - * @returns HashMap of tables or `WindowsError` - */ -export function parseTable( - path: string, - tables: string[], -): Record | WindowsError { - try { - //@ts-ignore: Custom Artemis function - const data = Deno.core.ops.get_table(path, tables); - - const results: Record = JSON.parse(data); - return results; - } catch (err) { - return new WindowsError("ESE", `failed to parse ese ${path}: ${err}`); +export class EseDatabase { + private path: string; + + /** + * Construct a EseDatabase class to interact with a Windows ESE database + * @param path Path to the ESE database + */ + constructor(path: string) { + this.path = path; + } + + /** + * Function to extract the Catalog from an ESE database + * @returns Array of `Catalog` entries + */ + public catalogInfo(): Catalog[] | WindowsError { + try { + //@ts-ignore: Custom Artemis function + const data = Deno.core.ops.get_catalog(this.path); + + const results: Catalog[] = JSON.parse(data); + return results; + } catch (err) { + return new WindowsError( + "ESE", + `failed to parse ese catalog ${this.path}: ${err}`, + ); + } + } + + /** + * Function to extract table metadata from the ESE Catalog + * @param catalog Array of `Catalog` entries + * @param table_name Table name to get info + * @returns `TableInfo` object + */ + public tableInfo(catalog: Catalog[], table_name: string): TableInfo { + const info: TableInfo = { + obj_id_table: 0, + table_page: 0, + table_name: "", + column_info: [], + long_value_page: 0, + }; + + for (const entry of catalog) { + if (entry.name === table_name) { + info.table_name = entry.name; + info.obj_id_table = entry.obj_id_table; + info.table_page = entry.column_or_father_data_page; + continue; + } + + if ( + entry.obj_id_table === info.obj_id_table && + info.table_name.length != 0 && + entry.catalog_type === CatalogType.Column + ) { + const column_info: ColumnInfo = { + column_type: this.getColumnType(entry.column_or_father_data_page), + column_name: entry.name, + column_data: [], + column_id: entry.id, + column_flags: this.getColumnFlags(entry.flags), + column_space_usage: entry.space_usage, + column_tagged_flags: [], + }; + + info.column_info.push(column_info); + } else if ( + entry.obj_id_table === info.obj_id_table && + info.table_name.length != 0 && + entry.catalog_type === CatalogType.LongValue + ) { + info.long_value_page = entry.column_or_father_data_page; + } + } + + return info; + } + + /** + * Function to get all pages associated with a table + * @param first_page The first page of a table. Can be found using `tableInfo` function + * @returns Array of page numbers or `WindowsError` + */ + public getPages(first_page: number): number[] | WindowsError { + try { + //@ts-ignore: Custom Artemis function + const data = Deno.core.ops.get_pages(this.path, first_page); + + const results: number[] = data; + return results; + } catch (err) { + return new WindowsError( + "ESE", + `failed to parse ese pages ${this.path}: ${err}`, + ); + } + } + + /** + * Function to extract rows from ESE database table + * @param pages Array of pages to use to get ESE rows + * @param info `TableInfo` object + * @param name Name of table to parse + * @returns HashMap of table data `Record` + */ + public getRows( + pages: number[], + info: TableInfo, + name: string, + ): Record | WindowsError { + try { + //@ts-ignore: Custom Artemis function + const data = Deno.core.ops.page_data( + this.path, + pages, + JSON.stringify(info), + name, + ); + + const results: Record = JSON.parse(data); + return results; + } catch (err) { + return new WindowsError( + "ESE", + `failed to parse ese rows ${this.path}: ${err}`, + ); + } + } + + /** + * Function to extract and filter rows from ESE database table. Useful if you want to combine tables based on shared key or you want to search for something + * @param pages Array of pages to use to get ESE rows + * @param info `TableInfo` object + * @param name Name of table to parse + * @param column_name Name of column to filter on + * @param column_data HashMap of column values to filter on `Record`. Only the key matters for filtering + * @returns + */ + public getFilteredRows( + pages: number[], + info: TableInfo, + name: string, + column_name: string, + column_data: Record, + ): Record | WindowsError { + try { + //@ts-ignore: Custom Artemis function + const data = Deno.core.ops.filter_page_data( + this.path, + pages, + JSON.stringify(info), + name, + column_name, + column_data, + ); + + const results: Record = JSON.parse(data); + return results; + } catch (err) { + return new WindowsError( + "ESE", + `failed to parse ese rows ${this.path}: ${err}`, + ); + } + } + + /** + * Function to dump specific columns from an ESE database table. Useful if you do **not** want all table columns provided from `getRows()` function. + * @param pages Array of pages to use to get ESE rows + * @param info `TableInfo` object + * @param name Name of table to parse + * @param column_names Array of columns to parse + * @returns + */ + public dumpTableColumns( + pages: number[], + info: TableInfo, + name: string, + column_names: string[], + ): Record | WindowsError { + try { + //@ts-ignore: Custom Artemis function + const data = Deno.core.ops.get_table_columns( + this.path, + pages, + JSON.stringify(info), + name, + column_names, + ); + + const results: Record = JSON.parse(data); + return results; + } catch (err) { + return new WindowsError( + "ESE", + `failed to parse ese rows ${this.path}: ${err}`, + ); + } + } + + /** + * Determine table column type + * @param column_type Column type value + * @returns `ColumnType` enum + */ + private getColumnType(column_type: number): ColumnType { + switch (column_type) { + case 0: + return ColumnType.Nil; + case 1: + return ColumnType.Bit; + case 2: + return ColumnType.UnsignedByte; + case 3: + return ColumnType.Short; + case 4: + return ColumnType.Long; + case 5: + return ColumnType.Currency; + case 6: + return ColumnType.Float32; + case 7: + return ColumnType.Float64; + case 8: + return ColumnType.DateTime; + case 9: + return ColumnType.Binary; + case 10: + return ColumnType.Text; + case 11: + return ColumnType.LongBinary; + case 12: + return ColumnType.LongText; + case 13: + return ColumnType.SuperLong; + case 14: + return ColumnType.UnsignedLong; + case 15: + return ColumnType.LongLong; + case 16: + return ColumnType.Guid; + case 17: + return ColumnType.UnsignedShort; + default: + return ColumnType.Unknown; + } + } + + /** + * Determine flags associated with table column + * @param flags Column flag value + * @returns Array of `ColumnFlags` + */ + private getColumnFlags(flags: number): ColumnFlags[] { + const not_null = 0x1; + const version = 0x2; + const increment = 0x4; + const multi = 0x8; + const flag_default = 0x10; + const escrow = 0x20; + const finalize = 0x40; + const user_define = 0x80; + const template = 0x100; + const delete_zero = 0x200; + const primary = 0x800; + const compressed = 0x1000; + const encrypted = 0x2000; + const versioned = 0x10000; + const deleted = 0x20000; + const version_add = 0x40000; + + const flags_data: ColumnFlags[] = []; + if ((flags & not_null) === not_null) { + flags_data.push(ColumnFlags.NotNull); + } + if ((flags & version) === version) { + flags_data.push(ColumnFlags.Version); + } + if ((flags & increment) === increment) { + flags_data.push(ColumnFlags.AutoIncrement); + } + if ((flags & multi) === multi) { + flags_data.push(ColumnFlags.MultiValued); + } + if ((flags & flag_default) === flag_default) { + flags_data.push(ColumnFlags.Default); + } + if ((flags & escrow) === escrow) { + flags_data.push(ColumnFlags.EscrowUpdate); + } + if ((flags & finalize) === finalize) { + flags_data.push(ColumnFlags.Finalize); + } + if ((flags & user_define) === user_define) { + flags_data.push(ColumnFlags.UserDefinedDefault); + } + if ((flags & template) === template) { + flags_data.push(ColumnFlags.TemplateColumnESE98); + } + if ((flags & delete_zero) === delete_zero) { + flags_data.push(ColumnFlags.DeleteOnZero); + } + if ((flags & primary) === primary) { + flags_data.push(ColumnFlags.PrimaryIndexPlaceholder); + } + if ((flags & compressed) === compressed) { + flags_data.push(ColumnFlags.Compressed); + } + if ((flags & encrypted) === encrypted) { + flags_data.push(ColumnFlags.Encrypted); + } + if ((flags & versioned) === versioned) { + flags_data.push(ColumnFlags.Versioned); + } + if ((flags & deleted) === deleted) { + flags_data.push(ColumnFlags.Deleted); + } + if ((flags & version_add) === version_add) { + flags_data.push(ColumnFlags.VersionedAdd); + } + return flags_data; } } diff --git a/src/windows/ese/ual.ts b/src/windows/ese/ual.ts index ff259106..0d8994e4 100644 --- a/src/windows/ese/ual.ts +++ b/src/windows/ese/ual.ts @@ -1,186 +1,272 @@ -import { getEnvValue } from "../../environment/env.ts"; -import { FileError } from "../../filesystem/errors.ts"; -import { glob } from "../../filesystem/files.ts"; import { WindowsError } from "../errors.ts"; -import { parseTable } from "../ese.ts"; +import { EseDatabase } from "../ese.ts"; import { UserAccessLog } from "../../../types/windows/ese/ual.ts"; -import { EseTable } from "../../../types/windows/ese.ts"; +import { EseTable, TableInfo } from "../../../types/windows/ese.ts"; import { decode } from "../../encoding/base64.ts"; import { EncodingError } from "../../encoding/errors.ts"; +interface RoleIds { + guid: string; + name: string; +} + /** - * Parse the UserAccessLog (UAL) database for logon activity. This database only exists on Windows Servers. Its an ESE database. - * It is **not** related to M365 UAL (Unified Audit Logging)! - * @param alt_dir Optional alternative directory to the UAL database. Will use default path at `%SYSTEMROOT%\System32\LogFiles\Sum` if not provided - * @returns Array of `UserAccessLog` entries or `WindowsError` + * Class to parse Windows User Access Log */ -export function userAccessLog( - alt_dir?: string, -): UserAccessLog[] | WindowsError { - const default_path = getEnvValue("SystemRoot"); +export class UserAccessLogging extends EseDatabase { + private info: TableInfo; + pages: number[]; - if (default_path === "") { - return new WindowsError("UAL", `failed to determine SystemRoot`); - } - let path = `${default_path}\\System32\\LogFiles\\Sum`; - if (alt_dir != undefined) { - if (alt_dir.endsWith("\\")) { - path = alt_dir.substring(0, alt_dir.length - 1); + /** + * Construct a `UserAccessLogging` object based on the provided UAL file. Client.mdb and .mdb files contain the logon information. SystemIdentity.mdb contains role information + * @param path Path to UAL related file. Such as SystemIdentity.mdb or Current.mdb or .mdb. + */ + constructor(path: string) { + super(path); + this.info = { + obj_id_table: 0, + table_page: 0, + table_name: "", + column_info: [], + long_value_page: 0, + }; + this.pages = []; + + if (path.endsWith("SystemIdentity.mdb")) { + this.setupRoleIds(); } else { - path = alt_dir; + this.setupClients(); } } - const path_glob = `${path}\\*.mdb`; + /** + * Function to get UAL RoleID info + * @param pages Array of pages to parse for the RoleIds table + * @returns Array of `RoleIds` or `WindowsError` + */ + public getRoleIds(pages: number[]): RoleIds[] | WindowsError { + if (this.info.table_name === "") { + return new WindowsError( + `UAL`, + `RoleIds info object not initialized property. Table name is empty`, + ); + } + const rows = this.getRows(pages, this.info, "ROLE_IDS"); + if (rows instanceof WindowsError) { + return rows; + } - // Glob for all MDB files. There may be up to four (4) - const paths = glob(path_glob); - if (paths instanceof FileError) { - return new WindowsError("UAL", `failed to glob paths ${paths}`); + return this.parseIds(rows["ROLE_IDS"]); } - const current_tables = ["CLIENTS"]; - const id_tables = ["ROLE_IDS"]; - - let entries: UserAccessLog[] = []; - let ids: RoleIds[] = []; - for (const path of paths) { - if (path.filename === "SystemIdentity.mdb") { - const rows = parseTable(path.full_path, id_tables); - if (rows instanceof WindowsError) { - console.warn(`Failed to parse SystemIdentity.mdb file: ${rows}`); - continue; - } - ids = parseIds(rows["ROLE_IDS"]); - continue; + /** + * Function to parse the UserAccessLog and do optional Role lookups + * @param pages Array of pages to parse for the Clients table + * @param roles_ual Optional `UserAccessLogging` object associated with the `SystemIdentity.mdb` file. Will be used to lookup role names. If none provided then no lookups will be done + * @param role_page_chunk Optional page limit for looking up Roles in the `SystemIdentity.mdb` file. Default is 30 page limit + * @returns Array of `UserAccessLog` or `WindowsError` + */ + public getUserAccessLog( + pages: number[], + roles_ual?: UserAccessLogging, + role_page_chunk = 30, + ): UserAccessLog[] | WindowsError { + if (this.info.table_name === "") { + return new WindowsError( + `UAL`, + `Clients info object not initialized property. Table name is empty`, + ); } - - const rows = parseTable(path.full_path, current_tables); + const rows = this.getRows(pages, this.info, "CLIENTS"); if (rows instanceof WindowsError) { - console.warn(`Failed to parse ${path.full_path} file: ${rows}`); - continue; + return rows; } - entries = entries.concat(parseClients(rows["CLIENTS"])); - } - // Match Role GUIDs with names for users - for (const id of ids) { - for (const entry of entries) { - if (entry.role_guid != id.guid) { + const clients = this.parseClients(rows["CLIENTS"]); + let roles_limit: number[] = []; + if (roles_ual === undefined) { + return clients; + } + + for (const role_page of roles_ual.pages) { + roles_limit.push(role_page); + if (role_page_chunk != roles_limit.length) { continue; } - entry.role_name = id.name; + const roles = roles_ual.getRoleIds(roles_limit); + if (roles instanceof WindowsError) { + console.warn(`Failed to parse RoleIds: ${roles}. Returning data now`); + return clients; + } + // Match Role GUIDs with names for users + for (const id of roles) { + for (const entry of clients) { + if (entry.role_guid != id.guid) { + continue; + } + + entry.role_name = id.name; + } + } + roles_limit = []; } - } - return entries; -} + if (roles_limit.length != 0) { + const roles = roles_ual.getRoleIds(roles_limit); + if (roles instanceof WindowsError) { + console.warn(`Failed to parse RoleIds: ${roles}. Returning data now`); + return clients; + } + // Match Role GUIDs with names for users + for (const id of roles) { + for (const entry of clients) { + if (entry.role_guid != id.guid) { + continue; + } -/** - * Extract client info from UAL database - * @param rows Double array of `EseTable` entries. Represent ESE rows - * @returns Array of `UserAccessLog` entries - */ -function parseClients(rows: EseTable[][]): UserAccessLog[] { - const logs = []; - for (const row of rows) { - const ual_log: UserAccessLog = { - total_accesses: 0, - last_logon: 0, - first_logon: 0, - ip: "", - username: "", - domain: "", - domain_username: "", - role_guid: "", - role_name: "", - }; - for (const entry of row) { - switch (entry.column_name) { - case "RoleGuid": - ual_log.role_guid = entry.column_data; - break; - case "TotalAccesses": - ual_log.total_accesses = Number(entry.column_data); - break; - case "InsertDate": - ual_log.first_logon = Number(entry.column_data); - break; - case "LastAccess": - ual_log.last_logon = Number(entry.column_data); - break; - case "Address": - ual_log.ip = extractIp(entry.column_data); - break; - case "AuthenticatedUserName": { - ual_log.domain_username = entry.column_data; - const split = entry.column_data.split("\\"); - ual_log.domain = split.at(0) ?? ""; - ual_log.username = split.at(1) ?? ""; - break; + entry.role_name = id.name; } - - default: - break; } } - logs.push(ual_log); + + return clients; } - return logs; -} -interface RoleIds { - guid: string; - name: string; -} + /** + * Sets up the parser for getting UAL Role ID info + * @returns `WindowsError` or nothing + */ + private setupRoleIds() { + const catalog = this.catalogInfo(); + if (catalog instanceof WindowsError) { + return; + } -/** - * Extract role information from UAL database - * @param rows Double array of `EseTable` entries. Represent ESE rows - * @returns Array of `RoleIds` - */ -function parseIds(rows: EseTable[][]): RoleIds[] { - const roles = []; + this.info = this.tableInfo(catalog, "ROLE_IDS"); + const pages = this.getPages(this.info.table_page); + if (pages instanceof WindowsError) { + return; + } - for (const row of rows) { - const role: RoleIds = { - guid: "", - name: "", - }; - for (const entry of row) { - if (entry.column_name === "RoleGuid") { - role.guid = entry.column_data; - } else if (entry.column_name == "RoleName") { - role.name = entry.column_data; - } + this.pages = pages; + } + + private setupClients(): void | WindowsError { + const catalog = this.catalogInfo(); + if (catalog instanceof WindowsError) { + return; } - roles.push(role); + + this.info = this.tableInfo(catalog, "CLIENTS"); + const pages = this.getPages(this.info.table_page); + if (pages instanceof WindowsError) { + return; + } + + this.pages = pages; } - return roles; -} + /** + * Extract role information from UAL database + * @param rows Double array of `EseTable` entries. Represent ESE rows + * @returns Array of `RoleIds` + */ + private parseIds(rows: EseTable[][]): RoleIds[] { + const roles = []; -/** - * Simple function to extract IPv4 or IPv6 string from UAL - * @param encoded_ip Base64 encoded string that contains IP information - * @returns Human readable IP string - */ -function extractIp(encoded_ip: string): string { - const raw_ip = decode(encoded_ip); - if (raw_ip instanceof EncodingError) { - console.warn(`Could not base64 decode IP data: ${raw_ip}`); - return encoded_ip; + for (const row of rows) { + const role: RoleIds = { + guid: "", + name: "", + }; + for (const entry of row) { + if (entry.column_name === "RoleGuid") { + role.guid = entry.column_data; + } else if (entry.column_name == "RoleName") { + role.name = entry.column_data; + } + } + roles.push(role); + } + + return roles; } - const is_ipv6 = 16; - if (raw_ip.length === is_ipv6) { - const ip = []; - for (const data of raw_ip) { - ip.push(data.toString(16)); + /** + * Extract client info from UAL database + * @param rows Double array of `EseTable` entries. Represent ESE rows + * @returns Array of `UserAccessLog` entries + */ + private parseClients(rows: EseTable[][]): UserAccessLog[] { + const logs = []; + for (const row of rows) { + const ual_log: UserAccessLog = { + total_accesses: 0, + last_logon: 0, + first_logon: 0, + ip: "", + username: "", + domain: "", + domain_username: "", + role_guid: "", + role_name: "", + }; + for (const entry of row) { + switch (entry.column_name) { + case "RoleGuid": + ual_log.role_guid = entry.column_data; + break; + case "TotalAccesses": + ual_log.total_accesses = Number(entry.column_data); + break; + case "InsertDate": + ual_log.first_logon = Number(entry.column_data); + break; + case "LastAccess": + ual_log.last_logon = Number(entry.column_data); + break; + case "Address": + ual_log.ip = this.extractIp(entry.column_data); + break; + case "AuthenticatedUserName": { + ual_log.domain_username = entry.column_data; + const split = entry.column_data.split("\\"); + ual_log.domain = split.at(0) ?? ""; + ual_log.username = split.at(1) ?? ""; + break; + } + + default: + break; + } + } + logs.push(ual_log); } - return ip.join(":"); + return logs; } - return raw_ip.join("."); + /** + * Simple function to extract IPv4 or IPv6 string from UAL + * @param encoded_ip Base64 encoded string that contains IP information + * @returns Human readable IP string + */ + private extractIp(encoded_ip: string): string { + const raw_ip = decode(encoded_ip); + if (raw_ip instanceof EncodingError) { + console.warn(`Could not base64 decode IP data: ${raw_ip}`); + return encoded_ip; + } + + const is_ipv6 = 16; + if (raw_ip.length === is_ipv6) { + const ip = []; + for (const data of raw_ip) { + ip.push(data.toString(16)); + } + return ip.join(":"); + } + + return raw_ip.join("."); + } } diff --git a/src/windows/ese/updates.ts b/src/windows/ese/updates.ts index 762a2a4d..d1b317c9 100644 --- a/src/windows/ese/updates.ts +++ b/src/windows/ese/updates.ts @@ -1,4 +1,4 @@ -import { EseTable } from "../../../types/windows/ese.ts"; +import { EseTable, TableInfo } from "../../../types/windows/ese.ts"; import { Operation, ServerSelection, @@ -10,168 +10,210 @@ import { formatGuid } from "../../encoding/uuid.ts"; import { getEnvValue } from "../../environment/env.ts"; import { Endian } from "../../nom/helpers.ts"; import { nomUnsignedFourBytes, take } from "../../nom/mod.ts"; +import { filetimeToUnixEpoch } from "../../time/conversion.ts"; import { WindowsError } from "../errors.ts"; -import { parseTable } from "../ese.ts"; +import { EseDatabase } from "../ese.ts"; /** - * Get the update history on a system - * @param path Optional path to `DataStore.edb` file. Defaults to SystemRoot env value - * @returns Array of `UpdateHistory` or `WindowsError` + * Class to parse history of Windows Updates */ -export function updateHistory( - alt_path?: string, -): UpdateHistory[] | WindowsError { - const default_path = getEnvValue("SystemRoot"); - if (default_path === "") { - return new WindowsError("UPDATESHISTORY", `failed determine SystemRoot`); +export class Updates extends EseDatabase { + private info: TableInfo; + pages: number[]; + + private table = "tbHistory"; + + /** + * Contruct `Updates` to parse Windows Updates history. By default will parse updates at `\Windows\SoftwareDistribution\DataStore\DataStore.edb`. Unless you specify alternative file. + * @param alt_path Optional alternative path to `DataStore.edb` + */ + constructor(alt_path?: string) { + const default_path = getEnvValue("SystemDrive"); + let path = + `${default_path}\\Windows\\SoftwareDistribution\\DataStore\\DataStore.edb`; + if (alt_path != undefined && alt_path.endsWith("DataStore.edb")) { + path = alt_path; + } + + super(path); + this.info = { + obj_id_table: 0, + table_page: 0, + table_name: "", + column_info: [], + long_value_page: 0, + }; + this.pages = []; + this.setupHistory(); } - let path = `${default_path}\\SoftwareDistribution\\DataStore.edb`; - if (alt_path != undefined && alt_path.endsWith("DataStore.edb")) { - path = alt_path; + + /** + * Function to parse Windows Updates History + * @param pages Array of pages to parse for the update history table + * @returns Array of `UpdateHistory` or `WindowsError` + */ + public updateHistory(pages: number[]): UpdateHistory[] | WindowsError { + if (this.info.table_name === "") { + return new WindowsError( + `UPDATESHISTORY`, + `tbHistory info object not initialized property. Table name is empty`, + ); + } + const rows = this.getRows(pages, this.info, this.table); + if (rows instanceof WindowsError) { + return rows; + } + + return this.parseHistory(rows[this.table]); } - const table = ["tbHistory"]; - const rows = parseTable(path, table); - if (rows instanceof WindowsError) { - return new WindowsError( - "UPDATESHISTORY", - `failed to parse ESE ${path}: ${rows.message}`, - ); + + private setupHistory() { + const catalog = this.catalogInfo(); + if (catalog instanceof WindowsError) { + return; + } + + this.info = this.tableInfo(catalog, this.table); + const pages = this.getPages(this.info.table_page); + if (pages instanceof WindowsError) { + return; + } + + this.pages = pages; } - return parseHistory(rows["tbHistory"]); -} + /** + * Parse the rows and columns of the `tbHistory` table + * @param rows Nested Array of `EseTable` rows + * @returns Array of `UpdateHistory` entries + */ + private parseHistory(rows: EseTable[][]): UpdateHistory[] { + const udpates = []; -/** - * Parse the rows and columns of the `tbHistory` table - * @param rows Nested Array of `EseTable` rows - * @returns Array of `UpdateHistory` entries - */ -function parseHistory(rows: EseTable[][]): UpdateHistory[] { - const udpates = []; - - for (const row of rows) { - const update: UpdateHistory = { - client_id: "", - support_url: "", - date: 0, - description: "", - operation: Operation.Unknown, - server_selection: ServerSelection.Unknown, - service_id: "", - title: "", - update_id: "", - update_revision: 0, - categories: "", - more_info: "", - }; - for (const column of row) { - if (column.column_name === "ClientId") { - update.client_id = column.column_data; - } else if (column.column_name === "SupportUrl") { - update.support_url = column.column_data; - } else if (column.column_name === "Title") { - update.title = column.column_data; - } else if (column.column_name === "Description") { - update.description = column.column_data; - } else if (column.column_name === "Categories") { - update.categories = column.column_data; - } else if (column.column_name === "MoreInfoUrl") { - update.more_info = column.column_data; - } else if (column.column_name === "Date") { - update.date = Number(column.column_data); - } else if (column.column_name === "Status") { - switch (column.column_data) { - case "1": { - update.operation = Operation.Installation; - break; + for (const row of rows) { + const update: UpdateHistory = { + client_id: "", + support_url: "", + date: 0, + description: "", + operation: Operation.Unknown, + server_selection: ServerSelection.Unknown, + service_id: "", + title: "", + update_id: "", + update_revision: 0, + categories: "", + more_info: "", + }; + for (const column of row) { + if (column.column_name === "ClientId") { + update.client_id = column.column_data; + } else if (column.column_name === "SupportUrl") { + update.support_url = column.column_data; + } else if (column.column_name === "Title") { + update.title = column.column_data; + } else if (column.column_name === "Description") { + update.description = column.column_data; + } else if (column.column_name === "Categories") { + update.categories = column.column_data; + } else if (column.column_name === "MoreInfoUrl") { + update.more_info = column.column_data; + } else if (column.column_name === "Date") { + update.date = filetimeToUnixEpoch(BigInt(column.column_data)); + } else if (column.column_name === "Status") { + switch (column.column_data) { + case "1": { + update.operation = Operation.Installation; + break; + } + case "2": { + update.operation = Operation.Uninstallation; + break; + } } - case "2": { - update.operation = Operation.Uninstallation; - break; + } else if (column.column_name === "UpdateId") { + const update_info = this.getUpdateId(column.column_data); + if (update_info instanceof Error) { + console.warn(`could not parse update id info ${update_info}`); + } else { + update.update_id = update_info["guid"] as string; + update.update_revision = update_info["revision"] as number; } + } else if (column.column_name === "ServerId") { + const update_info = this.getUpdateId(column.column_data); + if (update_info instanceof Error) { + console.warn(`could not parse service id info ${update_info}`); + } else { + update.service_id = update_info["guid"] as string; + } + } else if (column.column_name === "ServerSelection") { + update.server_selection = this.getServerSelection(column.column_data); } - } else if (column.column_name === "UpdateId") { - const update_info = getUpdateId(column.column_data); - if (update_info instanceof Error) { - console.warn(`could not parse update id info ${update_info}`); - } else { - update.update_id = update_info["guid"] as string; - update.update_revision = update_info["revision"] as number; - } - } else if (column.column_name === "ServerId") { - const update_info = getUpdateId(column.column_data); - if (update_info instanceof Error) { - console.warn(`could not parse service id info ${update_info}`); - } else { - update.service_id = update_info["guid"] as string; - } - } else if (column.column_name === "ServerSelection") { - update.server_selection = getServerSelection(column.column_data); } + udpates.push(update); } - udpates.push(update); + return udpates; } - return udpates; -} -/** - * Get update id information - * @param data column data to parse - * @returns Object containing a GUID and revision number - */ -function getUpdateId(data: string): Record | Error { - const input = decode(data); - if (input instanceof EncodingError) { - return input; - } - const guid_size = 16; - const guid = take(input, guid_size); - if (guid instanceof Error) { - return guid; - } + /** + * Get update id information + * @param data column data to parse + * @returns Object containing a GUID and revision number + */ + private getUpdateId(data: string): Record | Error { + const input = decode(data); + if (input instanceof EncodingError) { + return input; + } + const guid_size = 16; + const guid = take(input, guid_size); + if (guid instanceof Error) { + return guid; + } - const update_id: Record = {}; - update_id["guid"] = formatGuid(Endian.Le, guid.nommed as Uint8Array); + const update_id: Record = {}; + update_id["guid"] = formatGuid(Endian.Le, guid.nommed as Uint8Array); - if (guid.remaining.length === 0) { - return update_id; - } + if (guid.remaining.length === 0) { + return update_id; + } - const revision_data = nomUnsignedFourBytes( - guid.remaining as Uint8Array, - Endian.Le, - ); - if (revision_data instanceof Error) { - return revision_data; - } + const revision_data = nomUnsignedFourBytes( + guid.remaining as Uint8Array, + Endian.Le, + ); + if (revision_data instanceof Error) { + return revision_data; + } - update_id["revision"] = revision_data.value; - return update_id; -} + update_id["revision"] = revision_data.value; + return update_id; + } -/** - * Determine the server selection status - * @param selection column data to parse - * @returns `ServerSelection` status - */ -function getServerSelection(selection: string): ServerSelection { - const value = Number(selection); - // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/aa387280(v=vs.85) - switch (value - 1) { - case 0: { - return ServerSelection.Default; - } - case 1: { - return ServerSelection.ManagedServer; - } - case 2: { - return ServerSelection.WindowsUpdate; - } - case 3: { - return ServerSelection.Others; - } - default: { - return ServerSelection.Unknown; + /** + * Determine the server selection status + * @param selection column data to parse + * @returns `ServerSelection` status + */ + private getServerSelection(selection: string): ServerSelection { + const value = Number(selection); + // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/aa387280(v=vs.85) + switch (value - 1) { + case 0: { + return ServerSelection.Default; + } + case 1: { + return ServerSelection.ManagedServer; + } + case 2: { + return ServerSelection.WindowsUpdate; + } + case 3: { + return ServerSelection.Others; + } + default: { + return ServerSelection.Unknown; + } } } } diff --git a/types/windows/bits.ts b/types/windows/bits.ts index 6b8a9e61..3c7e91f5 100644 --- a/types/windows/bits.ts +++ b/types/windows/bits.ts @@ -30,8 +30,6 @@ export interface BitsInfo { file_id: string; /**SID associated with the Job */ owner_sid: string; - /**Username associated with SID */ - username: string; /**Timestamp when the Job was created in UNIXEPOCH seconds */ created: number; /**Timestamp when the Job was modified in UNIXEPOCH seconds */ @@ -102,8 +100,6 @@ export interface Jobs { file_id: string; /**SID associated with the Job */ owner_sid: string; - /**Username associated with SID */ - username: string; /**Timestamp when the Job was created in UNIXEPOCH seconds */ created: number; /**Timestamp when the Job was modified in UNIXEPOCH seconds */ @@ -155,8 +151,8 @@ export interface Files { /**Number of bytes downloaded */ download_bytes_size: number | bigint | string; /**Number of bytes transferred */ - trasfer_bytes_size: number | bigint | string; - /**Fulll file path associated with Job */ + transfer_bytes_size: number | bigint | string; + /**Full file path associated with Job */ full_path: string; /**Filename associated with Job */ filename: string; diff --git a/types/windows/ese.ts b/types/windows/ese.ts index e57bfa01..42558e0f 100644 --- a/types/windows/ese.ts +++ b/types/windows/ese.ts @@ -26,6 +26,126 @@ export enum ColumnType { UnsignedLong = "UnsignedLong", LongLong = "LongLong", Guid = "Guid", - UnsingedShort = "UnsingedShort", + UnsignedShort = "UnsignedShort", Unknown = "Unknown", } + + +/** + * Metadata about the ESE database Catalog + */ +export interface Catalog { + /**Fixed data */ + obj_id_table: number; + /**Fixed data */ + catalog_type: CatalogType; + /**Fixed data */ + id: number; + /** Fixed data - Column only if the `catalog_type` is Column, otherwise father data page (FDP) */ + column_or_father_data_page: number; + /**Fixed data */ + space_usage: number; + /**Fixed data - If `catalog_type` is Column then these are columns flags */ + flags: number; + /**Fixed data */ + pages_or_locale: number; + /**Fixed data */ + root_flag: number; + /**Fixed data */ + record_offset: number; + /**Fixed data */ + lc_map_flags: number; + /**Fixed data */ + key_most: number; + /**Fixed data */ + lv_chunk_max: number; + /**Variable data */ + name: string; + /**Variable data */ + stats: Uint8Array; + /**Variable data */ + template_table: string; + /**Variable data */ + default_value: Uint8Array; + /**Variable data */ + key_fld_ids: Uint8Array; + /**Variable data */ + var_seg_mac: Uint8Array; + /**Variable data */ + conditional_columns: Uint8Array; + /**Variable data */ + tuple_limits: Uint8Array; + /**Variable data */ + version: Uint8Array; + /**Variable data */ + sort_id: Uint8Array; + /**Tagged data */ + callback_data: Uint8Array; + /**Tagged data */ + callback_dependencies: Uint8Array; + /**Tagged data */ + separate_lv: Uint8Array; + /**Tagged data */ + space_hints: Uint8Array; + /**Tagged data */ + space_deferred_lv_hints: Uint8Array; + /**Tagged data */ + local_name: Uint8Array; +} + +export enum CatalogType { + Table = "Table", + Column = "Column", + Index = "Index", + LongValue = "LongValue", + Callback = "Callback", + SlvAvail = "SlvAvail", + SlvSpaceMap = "SlvSpaceMap", + Unknown = "Unknown", +} + +export interface TableInfo { + obj_id_table: number; + table_page: number; + table_name: string; + column_info: ColumnInfo[]; + long_value_page: number; +} + +export interface ColumnInfo { + column_type: ColumnType; + column_name: string; + column_data: number[]; + column_id: number; + column_flags: ColumnFlags[]; + column_space_usage: number; + column_tagged_flags: TaggedDataFlag[]; +} + +export enum ColumnFlags { + NotNull = "NotNull", + Version = "Version", + AutoIncrement = "AutoIncrement", + MultiValued = "MultiValued", + Default = "Default", + EscrowUpdate = "EscrowUpdate", + Finalize = "Finalize", + UserDefinedDefault = "UserDefinedDefault", + TemplateColumnESE98 = "TemplateColumnESE98", + DeleteOnZero = "DeleteOnZero", + PrimaryIndexPlaceholder = "PrimaryIndexPlaceholder", + Compressed = "Compressed", + Encrypted = "Encrypted", + Versioned = "Versioned", + Deleted = "Deleted", + VersionedAdd = "VersionedAdd", +} + +enum TaggedDataFlag { + Variable = "Variable", + Compressed = "Compressed", + LongValue = "LongValue", + MultiValue = "MultiValue", + MultiValueSizeDefinition = "MultiValueSizeDefinition", + Unknown = "Unknown", +} \ No newline at end of file diff --git a/types/windows/ese/updates.ts b/types/windows/ese/updates.ts index 1a5d3c91..f1cbf47a 100644 --- a/types/windows/ese/updates.ts +++ b/types/windows/ese/updates.ts @@ -1,6 +1,7 @@ export interface UpdateHistory { client_id: string; support_url: string; + /**Timestamp in UNIXEPOCH seconds */ date: number; description: string; operation: Operation; From c4f1750764d2ccdaf56d0540fe7d9d5940aff927 Mon Sep 17 00:00:00 2001 From: puffyCid <16283453+puffyCid@users.noreply.github.com> Date: Fri, 31 May 2024 23:17:34 -0400 Subject: [PATCH 2/8] doc updates and some tests --- .../Artifacts/Application Artifacts/safari.md | 10 +- .../Artifacts/macOS Artifacts/loginitems.md | 96 ++++++++++++++++++- tests/macos/tcc/build.ts | 16 ++++ tests/macos/tcc/main.ts | 15 +++ tests/macos/xprotect/build.ts | 16 ++++ tests/macos/xprotect/main.ts | 15 +++ types/macos/loginitems.ts | 96 ++++++++++++++++++- types/macos/safari.ts | 12 ++- 8 files changed, 256 insertions(+), 20 deletions(-) create mode 100644 tests/macos/tcc/build.ts create mode 100644 tests/macos/tcc/main.ts create mode 100644 tests/macos/xprotect/build.ts create mode 100644 tests/macos/xprotect/main.ts diff --git a/artemis-docs/docs/Artifacts/Application Artifacts/safari.md b/artemis-docs/docs/Artifacts/Application Artifacts/safari.md index ca78cd68..f5b6bca4 100644 --- a/artemis-docs/docs/Artifacts/Application Artifacts/safari.md +++ b/artemis-docs/docs/Artifacts/Application Artifacts/safari.md @@ -124,9 +124,9 @@ export interface RawSafariDownloads { /**Download finish date in UNIXEPOCH seoconds */ download_entry_finish: number; /**Path to file to run */ - path: string[]; + path: string; /**Path represented as Catalog Node ID */ - cnid_path: number[]; + cnid_path: number; /**Created timestamp of target file in UNIXEPOCH seconds */ created: number; /**Path to the volume of target file */ @@ -142,7 +142,7 @@ export interface RawSafariDownloads { /**Created timestamp of volume in UNIXEPOCH seconds */ volume_created: number; /**Volume Property flags */ - volume_flag: number[]; + volume_flag: VolumeFlags[]; /**Flag if volume if the root filesystem */ volume_root: boolean; /**Localized name of target file */ @@ -152,7 +152,7 @@ export interface RawSafariDownloads { /**Read-Only security extension of target file */ security_extension_ro: string; /**File property flags */ - target_flags: number[]; + target_flags: TargetFlags[]; /**Username associated with `Bookmark` */ username: string; /**Folder index number associated with target file */ @@ -160,7 +160,7 @@ export interface RawSafariDownloads { /**UID associated with `LoginItem` */ uid: number; /**`LoginItem` creation flags */ - creation_options: number; + creation_options: CreationFlags[]; /**Is target file executable */ is_executable: boolean; /**Does target file have file reference flag */ diff --git a/artemis-docs/docs/Artifacts/macOS Artifacts/loginitems.md b/artemis-docs/docs/Artifacts/macOS Artifacts/loginitems.md index 2cb1eb11..0c878aa9 100644 --- a/artemis-docs/docs/Artifacts/macOS Artifacts/loginitems.md +++ b/artemis-docs/docs/Artifacts/macOS Artifacts/loginitems.md @@ -61,9 +61,9 @@ An array of `LoginItem` entries ```typescript export interface LoginItems { /**Path to file to run */ - path: string[]; + path: string; /**Path represented as Catalog Node ID */ - cnid_path: number[]; + cnid_path: string; /**Created timestamp of target file in UNIXEPOCH seconds */ created: number; /**Path to the volume of target file */ @@ -79,7 +79,7 @@ export interface LoginItems { /**Created timestamp of volume in UNIXEPOCH seconds */ volume_created: number; /**Volume Property flags */ - volume_flag: number[]; + volume_flags: VolumeFlags[]; /**Flag if volume if the root filesystem */ volume_root: boolean; /**Localized name of target file */ @@ -89,7 +89,7 @@ export interface LoginItems { /**Read-Only security extension of target file */ security_extension_ro: string; /**File property flags */ - target_flags: number[]; + target_flags: TargetFlags[]; /**Username associated with `Bookmark` */ username: string; /**Folder index number associated with target file */ @@ -97,7 +97,7 @@ export interface LoginItems { /**UID associated with `LoginItem` */ uid: number; /**`LoginItem` creation flags */ - creation_options: number; + creation_options: CreationFlags[]; /**Is `LoginItem` bundled in app */ is_bundled: boolean; /**App ID associated with `LoginItem` */ @@ -111,4 +111,90 @@ export interface LoginItems { /**Path to `LoginItem` source */ source_path: string; } + +export enum TargetFlags { + RegularFile = "RegularFile", + Directory = "Directory", + SymbolicLink = "SymbolicLink", + Volume = "Volume", + Package = "Package", + SystemImmutable = "SystemImmutable", + UserImmutable = "UserImmutable", + Hidden = "Hidden", + HasHiddenExtension = "HasHiddenExtension", + Application = "Application", + Compressed = "Compressed", + CanSetHiddenExtension = "CanSetHiddenExtension", + Readable = "Readable", + Writable = "Writable", + Executable = "Executable", + AliasFile = "AliasFile", + MountTrigger = "MountTrigger", +} + +export enum CreationFlags { + MinimalBookmark = "MinimalBookmark", + SuitableBookmark = "SuitableBookmark", + SecurityScope = "SecurityScope", + SecurityScopeAllowOnlyReadAccess = "SecurityScopeAllowOnlyReadAccess", + WithoutImplicitSecurityScope = "WithoutImplicitSecurityScope", + PreferFileIDResolutionMask = "PreferFileIDResolutionMask", +} + +export enum VolumeFlags { + Local = "Local", + Automount = "Automount", + DontBrowse = "DontBrowse", + ReadOnly = "ReadOnly", + Quarantined = "Quarantined", + Ejectable = "Ejectable", + Removable = "Removable", + Internal = "Internal", + External = "External", + DiskImage = "DiskImage", + FileVault = "FileVault", + LocaliDiskMirror = "LocaliDiskMirror", + Ipod = "Ipod", + Idisk = "Idisk", + Cd = "Cd", + Dvd = "Dvd", + DeviceFileSystem = "DeviceFileSystem", + TimeMachine = "TimeMachine", + Airport = "Airport", + VideoDisk = "VideoDisk", + DvdVideo = "DvdVideo", + BdVideo = "BdVideo", + MobileTimeMachine = "MobileTimeMachine", + NetworkOptical = "NetworkOptical", + BeingRepaired = "BeingRepaired", + Unmounted = "Unmounted", + SupportsPersistentIds = "SupportsPersistentIds", + SupportsSearchFs = "SupportsSearchFs", + SupportsExchange = "SupportsExchange", + SupportsSymbolicLinks = "SupportsSymbolicLinks", + SupportsDenyModes = "SupportsDenyModes", + SupportsCopyFile = "SupportsCopyFile", + SupportsReadDirAttr = "SupportsReadDirAttr", + SupportsJournaling = "SupportsJournaling", + SupportsRename = "SupportsRename", + SupportsFastStatFs = "SupportsFastStatFs", + SupportsCaseSensitiveNames = "SupportsCaseSensitiveNames", + SupportsCasePreservedNames = "SupportsCasePreservedNames", + SupportsFlock = "SupportsFlock", + SupportsNoRootDirectoryTimes = "SupportsNoRootDirectoryTimes", + SupportsExtendedSecurity = "SupportsExtendedSecurity", + Supports2TbFileSize = "Supports2TbFileSize", + SupportsHardLinks = "SupportsHardLinks", + SupportsMandatoryByteRangeLocks = "SupportsMandatoryByteRangeLocks", + SupportsPathFromId = "SupportsPathFromId", + Journaling = "Journaling", + SupportsSparseFiles = "SupportsSparseFiles", + SupportsZeroRunes = "SupportsZeroRunes", + SupportsVolumeSizes = "SupportsVolumeSizes", + SupportsRemoteEvents = "SupportsRemoteEvents", + SupportsHiddenFiles = "SupportsHiddenFiles", + SupportsDecmpFsCompression = "SupportsDecmpFsCompression", + Has64BitObjectIds = "Has64BitObjectIds", + PropertyFlagsAll = "PropertyFlagsAll", +} ``` diff --git a/tests/macos/tcc/build.ts b/tests/macos/tcc/build.ts new file mode 100644 index 00000000..5d5ea713 --- /dev/null +++ b/tests/macos/tcc/build.ts @@ -0,0 +1,16 @@ +import * as esbuild from "https://deno.land/x/esbuild@v0.15.10/mod.js"; +import { denoPlugin } from "https://deno.land/x/esbuild_deno_loader@0.6.0/mod.ts"; + +async function main() { + const _result = await esbuild.build({ + plugins: [denoPlugin()], + entryPoints: ["./main.ts"], + outfile: "main.js", + bundle: true, + format: "cjs", + }); + + esbuild.stop(); +} + +main(); diff --git a/tests/macos/tcc/main.ts b/tests/macos/tcc/main.ts new file mode 100644 index 00000000..1e02678a --- /dev/null +++ b/tests/macos/tcc/main.ts @@ -0,0 +1,15 @@ +import { MacosError } from "../../../src/macos/errors.ts"; +import { queryTccDb } from "../../../src/macos/sqlite/tcc.ts"; + +function main() { + const data = queryTccDb(); + if (data instanceof MacosError) { + throw data; + } + + if (data.length === 0) { + throw "no tcc entries?!"; + } +} + +main(); diff --git a/tests/macos/xprotect/build.ts b/tests/macos/xprotect/build.ts new file mode 100644 index 00000000..5d5ea713 --- /dev/null +++ b/tests/macos/xprotect/build.ts @@ -0,0 +1,16 @@ +import * as esbuild from "https://deno.land/x/esbuild@v0.15.10/mod.js"; +import { denoPlugin } from "https://deno.land/x/esbuild_deno_loader@0.6.0/mod.ts"; + +async function main() { + const _result = await esbuild.build({ + plugins: [denoPlugin()], + entryPoints: ["./main.ts"], + outfile: "main.js", + bundle: true, + format: "cjs", + }); + + esbuild.stop(); +} + +main(); diff --git a/tests/macos/xprotect/main.ts b/tests/macos/xprotect/main.ts new file mode 100644 index 00000000..fd73c99f --- /dev/null +++ b/tests/macos/xprotect/main.ts @@ -0,0 +1,15 @@ +import { getXprotectDefinitions } from "../../../mod.ts"; +import { MacosError } from "../../../src/macos/errors.ts"; + +function main() { + const results = getXprotectDefinitions(); + if (results instanceof MacosError) { + throw results; + } + + if (results.length === 0) { + throw "no xprotect entries?!"; + } +} + +main(); diff --git a/types/macos/loginitems.ts b/types/macos/loginitems.ts index 1b971e21..3bf2f1f8 100644 --- a/types/macos/loginitems.ts +++ b/types/macos/loginitems.ts @@ -12,9 +12,9 @@ */ export interface LoginItems { /**Path to file to run */ - path: string[]; + path: string; /**Path represented as Catalog Node ID */ - cnid_path: number[]; + cnid_path: string; /**Created timestamp of target file in UNIXEPOCH seconds */ created: number; /**Path to the volume of target file */ @@ -30,7 +30,7 @@ export interface LoginItems { /**Created timestamp of volume in UNIXEPOCH seconds */ volume_created: number; /**Volume Property flags */ - volume_flag: number[]; + volume_flags: VolumeFlags[]; /**Flag if volume if the root filesystem */ volume_root: boolean; /**Localized name of target file */ @@ -40,7 +40,7 @@ export interface LoginItems { /**Read-Only security extension of target file */ security_extension_ro: string; /**File property flags */ - target_flags: number[]; + target_flags: TargetFlags[]; /**Username associated with `Bookmark` */ username: string; /**Folder index number associated with target file */ @@ -48,7 +48,7 @@ export interface LoginItems { /**UID associated with `LoginItem` */ uid: number; /**`LoginItem` creation flags */ - creation_options: number; + creation_options: CreationFlags[]; /**Is `LoginItem` bundled in app */ is_bundled: boolean; /**App ID associated with `LoginItem` */ @@ -62,3 +62,89 @@ export interface LoginItems { /**Path to `LoginItem` source */ source_path: string; } + +export enum TargetFlags { + RegularFile = "RegularFile", + Directory = "Directory", + SymbolicLink = "SymbolicLink", + Volume = "Volume", + Package = "Package", + SystemImmutable = "SystemImmutable", + UserImmutable = "UserImmutable", + Hidden = "Hidden", + HasHiddenExtension = "HasHiddenExtension", + Application = "Application", + Compressed = "Compressed", + CanSetHiddenExtension = "CanSetHiddenExtension", + Readable = "Readable", + Writable = "Writable", + Executable = "Executable", + AliasFile = "AliasFile", + MountTrigger = "MountTrigger", +} + +export enum CreationFlags { + MinimalBookmark = "MinimalBookmark", + SuitableBookmark = "SuitableBookmark", + SecurityScope = "SecurityScope", + SecurityScopeAllowOnlyReadAccess = "SecurityScopeAllowOnlyReadAccess", + WithoutImplicitSecurityScope = "WithoutImplicitSecurityScope", + PreferFileIDResolutionMask = "PreferFileIDResolutionMask", +} + +export enum VolumeFlags { + Local = "Local", + Automount = "Automount", + DontBrowse = "DontBrowse", + ReadOnly = "ReadOnly", + Quarantined = "Quarantined", + Ejectable = "Ejectable", + Removable = "Removable", + Internal = "Internal", + External = "External", + DiskImage = "DiskImage", + FileVault = "FileVault", + LocaliDiskMirror = "LocaliDiskMirror", + Ipod = "Ipod", + Idisk = "Idisk", + Cd = "Cd", + Dvd = "Dvd", + DeviceFileSystem = "DeviceFileSystem", + TimeMachine = "TimeMachine", + Airport = "Airport", + VideoDisk = "VideoDisk", + DvdVideo = "DvdVideo", + BdVideo = "BdVideo", + MobileTimeMachine = "MobileTimeMachine", + NetworkOptical = "NetworkOptical", + BeingRepaired = "BeingRepaired", + Unmounted = "Unmounted", + SupportsPersistentIds = "SupportsPersistentIds", + SupportsSearchFs = "SupportsSearchFs", + SupportsExchange = "SupportsExchange", + SupportsSymbolicLinks = "SupportsSymbolicLinks", + SupportsDenyModes = "SupportsDenyModes", + SupportsCopyFile = "SupportsCopyFile", + SupportsReadDirAttr = "SupportsReadDirAttr", + SupportsJournaling = "SupportsJournaling", + SupportsRename = "SupportsRename", + SupportsFastStatFs = "SupportsFastStatFs", + SupportsCaseSensitiveNames = "SupportsCaseSensitiveNames", + SupportsCasePreservedNames = "SupportsCasePreservedNames", + SupportsFlock = "SupportsFlock", + SupportsNoRootDirectoryTimes = "SupportsNoRootDirectoryTimes", + SupportsExtendedSecurity = "SupportsExtendedSecurity", + Supports2TbFileSize = "Supports2TbFileSize", + SupportsHardLinks = "SupportsHardLinks", + SupportsMandatoryByteRangeLocks = "SupportsMandatoryByteRangeLocks", + SupportsPathFromId = "SupportsPathFromId", + Journaling = "Journaling", + SupportsSparseFiles = "SupportsSparseFiles", + SupportsZeroRunes = "SupportsZeroRunes", + SupportsVolumeSizes = "SupportsVolumeSizes", + SupportsRemoteEvents = "SupportsRemoteEvents", + SupportsHiddenFiles = "SupportsHiddenFiles", + SupportsDecmpFsCompression = "SupportsDecmpFsCompression", + Has64BitObjectIds = "Has64BitObjectIds", + PropertyFlagsAll = "PropertyFlagsAll", +} diff --git a/types/macos/safari.ts b/types/macos/safari.ts index 70365ab9..d74c6813 100644 --- a/types/macos/safari.ts +++ b/types/macos/safari.ts @@ -1,3 +1,5 @@ +import { CreationFlags, TargetFlags, VolumeFlags } from "./loginitems.ts"; + /** * Safari history is stored in a SQLITE file. * `artemis` uses the `sqlite` crate to read the SQLITE file. It can even read the file if Safari is running. @@ -87,9 +89,9 @@ export interface RawSafariDownloads { /**Download finish date in UNIXEPOCH seoconds */ download_entry_finish: number; /**Path to file to run */ - path: string[]; + path: string; /**Path represented as Catalog Node ID */ - cnid_path: number[]; + cnid_path: number; /**Created timestamp of target file in UNIXEPOCH seconds */ created: number; /**Path to the volume of target file */ @@ -105,7 +107,7 @@ export interface RawSafariDownloads { /**Created timestamp of volume in UNIXEPOCH seconds */ volume_created: number; /**Volume Property flags */ - volume_flag: number[]; + volume_flag: VolumeFlags[]; /**Flag if volume if the root filesystem */ volume_root: boolean; /**Localized name of target file */ @@ -115,7 +117,7 @@ export interface RawSafariDownloads { /**Read-Only security extension of target file */ security_extension_ro: string; /**File property flags */ - target_flags: number[]; + target_flags: TargetFlags[]; /**Username associated with `Bookmark` */ username: string; /**Folder index number associated with target file */ @@ -123,7 +125,7 @@ export interface RawSafariDownloads { /**UID associated with `LoginItem` */ uid: number; /**`LoginItem` creation flags */ - creation_options: number; + creation_options: CreationFlags[]; /**Is target file executable */ is_executable: boolean; /**Does target file have file reference flag */ From f2a649de2aa20cdc733968d3e544f7c2f3ecdc9e Mon Sep 17 00:00:00 2001 From: puffyCid <16283453+puffyCid@users.noreply.github.com> Date: Fri, 31 May 2024 23:26:05 -0400 Subject: [PATCH 3/8] doc updates --- artemis-docs/docs/Artifacts/macOS Artifacts/files.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/artemis-docs/docs/Artifacts/macOS Artifacts/files.md b/artemis-docs/docs/Artifacts/macOS Artifacts/files.md index a7271eab..bd8780aa 100644 --- a/artemis-docs/docs/Artifacts/macOS Artifacts/files.md +++ b/artemis-docs/docs/Artifacts/macOS Artifacts/files.md @@ -15,6 +15,12 @@ Protection (SIP) protected files. Since a filelisting can be extremely large, every 100k entries artemis will output the data and then continue. +Artemis will skip +[firmlink](http://www.swiftforensics.com/2019/10/macos-1015-volumes-firmlink-magic.html) +paths on the system by checking for registered firmlink paths at: + +- /usr/share/firmlinks + Other Parsers: - Any tool that can recursively list files and directories From 5c39769e22375f6252036e933407aa9ecbab2a05 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 1 Jun 2024 19:14:27 -0400 Subject: [PATCH 4/8] updates --- .../docs/API/API Scenarios/acquire_files.md | 4 +- artemis-docs/docs/API/API Scenarios/ese.md | 353 ++++++++++++++++++ artemis-docs/docs/API/Artifacts/windows.md | 165 ++++---- .../docs/Artifacts/Windows Artfacts/ese.md | 166 ++++++-- mod.ts | 4 - src/windows/ese.ts | 14 +- src/windows/ese/ual.ts | 5 +- src/windows/ese/updates.ts | 2 +- types/windows/ese.ts | 2 +- 9 files changed, 600 insertions(+), 115 deletions(-) create mode 100644 artemis-docs/docs/API/API Scenarios/ese.md diff --git a/artemis-docs/docs/API/API Scenarios/acquire_files.md b/artemis-docs/docs/API/API Scenarios/acquire_files.md index 169ba431..65c7c502 100644 --- a/artemis-docs/docs/API/API Scenarios/acquire_files.md +++ b/artemis-docs/docs/API/API Scenarios/acquire_files.md @@ -37,7 +37,7 @@ export interface Output { compress: boolean; /**Endpoint ID */ endpoint_id: string; - /**ID for collection. Must be postive number */ + /**ID for collection. Must be positive number */ collection_id: number; /**Output type: local, azure, aws, or gcp */ output: OutputType; @@ -50,7 +50,7 @@ export interface Output { When acquiring files there are three caveats in regards to the Output object: -- Format setting. This is option not applied to file acquistions +- Format setting. This is option not applied to file acquisitions - Compressing setting. File acquisitions are always compressed regardless of this setting. - OutputType setting. Currently only local or GCP output types can be used. diff --git a/artemis-docs/docs/API/API Scenarios/ese.md b/artemis-docs/docs/API/API Scenarios/ese.md new file mode 100644 index 00000000..4b5f9b13 --- /dev/null +++ b/artemis-docs/docs/API/API Scenarios/ese.md @@ -0,0 +1,353 @@ +--- +description: How to extract data from ESE databases +--- + +# ESE Databases + +The Windows Extensible Storage Engine ([ESE](https://en.wikipedia.org/wiki/Extensible_Storage_Engine)) is an open source database file used by several Windows components. It is used by several different types of interesting forensic artifacts such as: + +- Windows BITS +- Windows Search + +Artemis allows analysts to extract and explorer ESE databases using the TypeScript API. +However, these database files may become very large. For example, the Windows Search database can range from 200MBs to 8GBs in size. + +So we must careful that we do not read all of the data into memory. + +Artemis provides a TypeScript EseDatabase class to help us parse and interact with ESE databases and giving us flexibility on much systems resources we can use. + +## ESE Parsing Guide + +Let walkthrough a scenario on we can leverage the API to extract data from the Windows [User Access Logging database](https://www.crowdstrike.com/blog/user-access-logging-ual-overview/) (UAL). +The guide below assumes you have cloned the artemis API repository to your local system. You may also import the API remotely. + +The functions in this guide are documented [here](../Artifacts/windows.md#ese-database-class) + +### Create a EseDatabase class instance + +Before we can parse a ESE database we need to initialize an instance of the EseDatabase class. This is not too difficult :) + +```typescript +import { EseDatabase } from "./artemis-api/src/windows/ese.ts"; + +function main() { + // Provide path to the UAL file + const path = "C:\\Windows\\System32\\LogFiles\\sum\\Current.mdb"; + + const ese = new EseDatabase(path); +} + +main(); +``` + +The above code initializes a new EseDatabase instance that we will use to parse the UAL database. + +### Extract the Catalog + +Before we can do any parsing of the ESE database must get the Catalog associated with the ESE database. +The Catalog is a special table in all ESE databases that contains metadata on all tables and columns in the database. + +There are 4 high level steps required in order to extract data from an ESE database: + +1. Parse and extract the Catalog +2. Get the metadata associated with the ESE table(s) we are interested in +3. Get an array of pages associated with the table. Pages contain the table data +4. Extract the table data based on the pages provided + +The code below shows how to extract the Catalog from the Current.mdb database. + +```typescript +import { EseDatabase } from "./artemis-api/src/windows/ese.ts"; +import { WindowsError } from "./artemis-api/src/windows/errors.ts"; + +function main() { + // Provide path to the UAL file + const path = "C:\\Windows\\System32\\LogFiles\\sum\\Current.mdb"; + + const ese = new EseDatabase(path); + + // Get array of Catalog entries + const catalog = ese.catalogInfo(); + if (catalog instanceof WindowsError) { + return catalog; + } +} + +main(); +``` + +As mentioned the Catalog contains metadata on all Tables and Columns in an ESE database. We can use this the help explore what kind of data exists in the database. + +```typescript +import { EseDatabase } from "./artemis-api/src/windows/ese.ts"; +import { WindowsError } from "./artemis-api/src/windows/errors.ts"; + +function main() { + // Provide path to the UAL file + const path = "C:\\Windows\\System32\\LogFiles\\sum\\Current.mdb"; + + const ese = new EseDatabase(path); + + // Get array of Catalog entries + const catalog = ese.catalogInfo(); + if (catalog instanceof WindowsError) { + return catalog; + } + + for (const entry of catalog) { + console.log(`${entry.name} - Catalog Type: ${entry.catalog_type}`); + } +} + +main(); +``` + +:::warning + +Make sure you are checking for errors when parsing the ESE database. If artemis encounters an error you will probably not be able to parse the entire database. +If artemis fails to parse the Catalog, then you will not be able to parse the database. + +::: + +The code above loops through the Catalog and prints out probably the most interesting properties in the object: + +- The name of the Catalog entry +- The CatalogType for that entry. + +The CatalogType will be one of the following: + +```typescript +export enum CatalogType { + Table = "Table", + Column = "Column", + Index = "Index", + LongValue = "LongValue", + Callback = "Callback", + SlvAvail = "SlvAvail", + SlvSpaceMap = "SlvSpaceMap", + Unknown = "Unknown", +} +``` + +Only the enums Table and Column are the most interesting. The remaining types are associated with the database internals. + +:::info + +All ESE objects that artemis returns are defined in the [ESE](../../Artifacts/Windows%20Artfacts/ese.md) artifact. +Do not worry too much about the large amount of objects, artemis will handle all of the complexity and heavy lifting for parsing the data. + +::: + +### Getting Table information + +Since we are parsing the Current.mdb database, we are mainly interesting the CLIENTS table. + +The code below shows how to extract metadata associated with the CLIENTS table. + +```typescript +import { EseDatabase } from "./artemis-api/src/windows/ese.ts"; +import { WindowsError } from "./artemis-api/src/windows/errors.ts"; + +function main() { + // Provide path to the UAL file + const path = "C:\\Windows\\System32\\LogFiles\\sum\\Current.mdb"; + + const ese = new EseDatabase(path); + + // Get array of Catalog entries + const catalog = ese.catalogInfo(); + if (catalog instanceof WindowsError) { + return catalog; + } + + const name = "CLIENTS"; + const info = ese.tableInfo(catalog, name); + console.log(info); +} + +main(); +``` + +The tableInfo function will extract all metadata from the Catalog that is associated with our table name (CLIENTS). + +### Get Pages associated with Table + +We are now at step 3 of the 4 step process. We now must get all of the pages associated with our table (CLIENTS). These pages will point to where our data is. + +```typescript +import { EseDatabase } from "./artemis-api/src/windows/ese.ts"; +import { WindowsError } from "./artemis-api/src/windows/errors.ts"; + +function main() { + // Provide path to the UAL file + const path = "C:\\Windows\\System32\\LogFiles\\sum\\Current.mdb"; + + const ese = new EseDatabase(path); + + // Get array of Catalog entries + const catalog = ese.catalogInfo(); + if (catalog instanceof WindowsError) { + return catalog; + } + + const name = "CLIENTS"; + const info = ese.tableInfo(catalog, name); + + const pages = ese.getPages(info.table_page); + if (pages instanceof WindowsError) { + return; + } + + console.log(pages.length); +} + +main(); +``` + +The code above will now also get all of the pages associated with the table CLIENTS! + +### Getting our data + +We are now at the last step in order to get our data! This last step is the most important, because **you** will decide how much memory artemis will use in order to parse the database to get our data. + +```typescript +import { EseDatabase } from "./artemis-api/src/windows/ese.ts"; +import { WindowsError } from "./artemis-api/src/windows/errors.ts"; + +function main() { + // Provide path to the UAL file + const path = "C:\\Windows\\System32\\LogFiles\\sum\\Current.mdb"; + + const ese = new EseDatabase(path); + + // Get array of Catalog entries + const catalog = ese.catalogInfo(); + if (catalog instanceof WindowsError) { + return catalog; + } + + const name = "CLIENTS"; + const info = ese.tableInfo(catalog, name); + + const pages = ese.getPages(info.table_page); + if (pages instanceof WindowsError) { + return; + } + + console.log(pages.length); + + // getRows() returns Record + const data = ese.getRows(pages, info); + if (data instanceof WindowsError) { + return; + } + + console.log(data["CLIENTS"].length); +} + +main(); +``` + +The code above calls the function getRows() which will get our data associated with CLIENTS table. + +:::warning + +The number of pages and table content will determine the amount of memory artemis uses. + +Ex: If a table has 5 columns and 1000 pages and provide 1000 pages to getRows(), artemis will return back all of the data. +This **may** be ok. If the 5 columns only have numbers or small text then it **probably** will not require a lot of memory. + +However, if each column contain 1MB of data and there are 1000 rows, then artemis will end up using a lot of memory. + +::: + +Since the Current.mdb database can potentially be very large we do not want to parse all pages at once. +We will need to parse them in chunks. + +```typescript +import { EseDatabase } from "./artemis-api/src/windows/ese.ts"; +import { WindowsError } from "./artemis-api/src/windows/errors.ts"; + +function main() { + // Provide path to the UAL file + const path = "C:\\Windows\\System32\\LogFiles\\sum\\Current.mdb"; + + const ese = new EseDatabase(path); + + // Get array of Catalog entries + const catalog = ese.catalogInfo(); + if (catalog instanceof WindowsError) { + return catalog; + } + + const name = "CLIENTS"; + const info = ese.tableInfo(catalog, name); + + const pages = ese.getPages(info.table_page); + if (pages instanceof WindowsError) { + return; + } + + console.log(pages.length); + + const chunk_limit = 80; + let page_chunks = []; + + // Instead of using all pages at once. Divide the pages into smaller chunks and parse them + for (const page of pages) { + if (page_chunks.length != chunk_limit) { + page_chunks.push(page); + continue; + } + // getRows() returns Record + const data = ese.getRows(page_chunks, info); + if (data instanceof WindowsError) { + return; + } + + console.log(data["CLIENTS"].length); + + // Go through all rows + for (const row of data["CLIENTS"]) { + // Go through all columns + for (const column of row) { + console.log( + `Name: ${column.column_name} - Type: ${column.column_type} - Data: ${column.column_data}` + ); + } + } + + page_chunks = []; + } + + // Just in case we have any leftover pages + if (page_chunks.length != 0) { + const data = ese.getRows(page_chunks, info); + if (data instanceof WindowsError) { + return; + } + + console.log(data["CLIENTS"].length); + } +} + +main(); +``` + +The above code puts our pages into smaller chunks before calling the function getRows(), this allows to us to get all of the data associated with the CLIENTS table while keeping memory usage low. + +Additional details on ColumnTypes and EseTable structure can be found [here](../../Artifacts/Windows%20Artfacts/ese.md) + +:::info + +There is no perfect number when deciding the number of pages to provide to getRows(). In general the higher the page number the faster artemis will be when parsing the database but at the cost of memory usage. + +If you do not know what kind of data is an ESE database table: + +- Review the TableInfo object! +- A safe page number would probably be between 50-100 + +For additional background, the Windows Search database contains almost 600 columns and can grow to be very very large. +Artemis uses 400 page chunks to parse the Search database, which uses ~800-900MBs of memory. + +::: diff --git a/artemis-docs/docs/API/Artifacts/windows.md b/artemis-docs/docs/API/Artifacts/windows.md index ab6517b6..36dd47bd 100644 --- a/artemis-docs/docs/API/Artifacts/windows.md +++ b/artemis-docs/docs/API/Artifacts/windows.md @@ -328,66 +328,67 @@ not super useful. | path | string | Path to Windows Registry file | | offset | number | Offset to Security Key | -### parseTable(path, tables) -> Record<string, EseTable[][]> | WindowsError - -Parse an ESE database table at provided path. Will return a HashMap of tables. -Where there string key is the table name. Table rows are returned in double -array where each row is an array. Will bypass locked files and works dirty or -clean ESE databases. - -:::warning - -Larger ESE databases will consume more memory and resources - -::: - -Sample output for one table (SmTbleSmp) that has two rows: - -```typescript -{ - "SmTblSmp": [ - [ - { - "column_type": "Float64", - "column_name": "SectionID", - "column_data": "1" - }, - { - "column_type": "LongBinary", - "column_name": "Name", - "column_data": "bABzAGEAYQBuAG8AbgB5AG0AbwB1AHMAbgBhAG0AZQBsAG8AbwBrAHUAcAA=" - }, - { - "column_type": "LongBinary", - "column_name": "Value", - "column_data": "MAAAAA==" - } - ], - [ - { - "column_type": "Float64", - "column_name": "SectionID", - "column_data": "1" - }, - { - "column_type": "LongBinary", - "column_name": "Name", - "column_data": "ZQBuAGEAYgBsAGUAZwB1AGUAcwB0AGEAYwBjAG8AdQBuAHQA" - }, - { - "column_type": "LongBinary", - "column_name": "Value", - "column_data": "MAAAAA==" - } - ] - ] -} -``` - -| Param | Type | Description | -| ------ | -------- | ---------------------------- | -| path | string | Path to Windows ESE database | -| tables | string[] | One or more tables to parse | +### ESE Database Class + +A basic class to help interact and extract data from ESE databases + +#### catalogInfo() -> Catalog[] | WindowsError + +Dump the Catalog metadata associated with an ESE database. Returns an array of Catalog entries or WindowsError + +#### tableInfo(catalog, table_name) -> TableInfo + +Extract table metadata from parsed Catalog entries based on provided table name + +| Param | Type | Description | +| ---------- | --------- | ------------------------ | +| catalog | Catalog[] | Array of Catalog entries | +| table_name | string | Name of table to extract | + +#### getPages(first_page) -> number[] | WindowsError + +Get an array of all pages associated with a table starting at the first page provided. First page can be found in the TableInfo object. + +| Param | Type | Description | +| ---------- | ------ | --------------------- | +| first_page | number | First page of a table | + +#### getRows(pages, info) -> Record<string, EseTable[][]> | WindowsError + +Get rows associated with provided TableInfo object and number of pages. A returns a `Record` or WindowsError. + +The table name is the Record string key. + +[EseTable](../../Artifacts/Windows%20Artfacts/ese.md) is an array of rows and columns representing ESE data. + +| Param | Type | Description | +| ----- | --------- | ---------------- | +| pages | number[] | Array of pages | +| info | TableInfo | TableInfo object | + +#### getFilteredRows(pages, info, column_name, column_data) -> Record<string, EseTable[][]> | WindowsError + +Get rows and filter based on provided column_name and column_data. This function can be useful if you want to get data from a table thats shares data with another table. +For example, if you call getRows() to get data associated with TableA and now you want to get data from TableB and both tables share a unique key. + +Its _a little_ similar to "select \* from tableB where columnX = Y" where Y is a unique key + +| Param | Type | Description | +| ----------- | ----------------------------- | ------------------------------------------------------------------------------ | +| pages | number[] | Array of pages | +| info | TableInfo | TableInfo object | +| column_name | string | Column name that you want to filter on | +| column_data | Record<string, boolean> | HashMap of column values to filter on. Only the key is used to filter the data | + +#### dumpTableColumns(pages, info, column_names) -> Record<string, EseTable[][]> | WindowsError + +Get rows based on specific columns names. This function is the same as getRows() except it will only return column names that included in column_names. + +| Param | Type | Description | +| ----------- | --------- | -------------------------------------- | +| pages | number[] | Array of pages | +| info | TableInfo | TableInfo object | +| column_name | string[] | Array of column names to get data from | ### getChocolateyInfo(alt_base) -> ChocolateyInfo[] | WindowsError @@ -400,16 +401,17 @@ An optional alternative base path can also be provided | -------- | ------ | --------------------------------- | | alt_base | string | Optional base path for Chocolatey | -### updateHistory(alt_path) -> UpdateHistory[] | WindowsError +### Updates class -Return a list of Windows Updates by parsing the Windows DataStore.edb database. -Will use the SystemRoot ENV value by default (C:\Windows). +A simple class to help dump the contents of the Windows DataStore.edb database. This class extends the EseDatabase class. -An optional alternative path to DataStore.edb can also be provided instead. +#### updateHistory(pages) -> UpdateHistory[] | WindowsError + +Return a list of Windows Updates by parsing the Windows DataStore.edb database. -| Param | Type | Description | -| -------- | ------ | ----------------------------------- | -| alt_path | string | Optional full path to DataStore.edb | +| Param | Type | Description | +| ----- | -------- | ------------------------------- | +| pages | number[] | Array of pages to get data from | ### powershellHistory(alt_path) -> History[] | History | WindowsError @@ -442,6 +444,35 @@ shellitems. | ----- | ---------- | ---------------------- | | data | Uint8Array | Raw bytes of shellitem | +### UserAccessLogging class + +A simple class to help extract data from the Windows User Access Log database. This class extends the EseDatabase class + +#### getRoleIds(pages) -> RoleIds[] | WindowsError + +Return an array of RoleIds associated with UAL database. This function expects the UserAccessLogging class to be initialized with the SystemIdentity.mdb database otherwise it will return no results. + +| Param | Type | Description | +| ----- | -------- | ------------------------------- | +| pages | number[] | Array of pages to get data from | + +#### getUserAccessLog(pages, roles_ual, role_page_chunk) -> UserAccessLog[] | WindowsError + +Parse the User Access Log (UAL) database on Windows Servers. This database +contains logon information for users on the system.\ +It is **not** related to M365 UAL (Unified Audit Logging)! + +This function expects the UserAccessLogging class to be initialized with the Current.mdb or `{GUID}.mdb` database otherwise it will return no results. + +You may provide an optional UserAccessLogging associated with SystemIdentity.mdb to perform RoleID lookups. Otherwise this table will parse the Current.mdb or `{GUID}.mdb` database. +You may also customize the number of pages that should be used when doing RoleID lookups, by default 30 pages will used. + +| Param | Type | Description | +| --------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| pages | number[] | Array of pages to get data from | +| roles_ual | UserAccessLogging | Optional UserAccessLogging object that was initialized with the file SystemIdentity.mdb. Can be used to perform RoleID lookups | +| role_page_chunk | number | Number of pages that should be submitted when doing RoleID lookups. By default 30 page chunks will be used to do lookup | + ### userAccessLog(alt_dir) -> UserAccessLog[] | WindowsError Parse the User Access Log (UAL) database on Windows Servers. This database diff --git a/artemis-docs/docs/Artifacts/Windows Artfacts/ese.md b/artemis-docs/docs/Artifacts/Windows Artfacts/ese.md index 1c10c52c..32ae025c 100644 --- a/artemis-docs/docs/Artifacts/Windows Artfacts/ese.md +++ b/artemis-docs/docs/Artifacts/Windows Artfacts/ese.md @@ -19,12 +19,6 @@ applications such as: Artemis supports parsing both unlocked and locked ESE databases. -:::warning - -Larger ESE databases will consume more memory and resources - -::: - # Collection You have to use the artemis [api](../../API/overview.md) in order to parse a @@ -33,36 +27,32 @@ single ESE database. # Sample API Script ```typescript -import { - parseTable, -} from "https://raw.githubusercontent.com/puffycid/artemis-api/master/mod.ts"; - -async function main() { - const path = "path to ese file"; - const tables = ["table1", "table2"]; - - const results = parseTable(path, tables); - - console.log(results); -} -``` +import { EseDatabase } from "./artemis-api/src/windows/ese.ts"; +import { WindowsError } from "./artemis-api/src/windows/errors.ts"; -# Output Structure +function main() { + // Provide path to the UAL file + const path = "C:\\Windows\\System32\\LogFiles\\sum\\Current.mdb"; -A `Record` object structure. Where `string` is your table -name and `EseTable[][]` is an array of rows and columns. + const ese = new EseDatabase(path); -```typescript -const data: EseTable[][] = []; + const catalog = ese.catalogInfo(); + if (catalog === WindowsError) { + return catalog; + } -for (const row of data) { - for (const column of row) { - console.log(column.column_name); + for (const entry of catalog) { + console.log(`${entry.name} - Catalog Type: ${entry.catalog_type}`); } } ``` +# Output Structures + +Depending on functions used the artemis API will returning the following objects + ```typescript +/** Generic Interface for dumping ESE tables */ export interface EseTable { column_type: ColumnType; column_name: string; @@ -79,6 +69,7 @@ export enum ColumnType { Currency = "Currency", Float32 = "Float32", Float64 = "Float64", + /** All timestamps have been converted to UNIXEPOCH seconds */ DateTime = "DateTime", Binary = "Binary", /** Can be ASCII or Unicode */ @@ -90,7 +81,126 @@ export enum ColumnType { UnsignedLong = "UnsignedLong", LongLong = "LongLong", Guid = "Guid", - UnsingedShort = "UnsingedShort", + UnsignedShort = "UnsignedShort", + Unknown = "Unknown", +} + +/** + * Metadata about the ESE database Catalog + */ +export interface Catalog { + /**Fixed data */ + obj_id_table: number; + /**Fixed data */ + catalog_type: CatalogType; + /**Fixed data */ + id: number; + /** Fixed data - Column only if the `catalog_type` is Column, otherwise father data page (FDP) */ + column_or_father_data_page: number; + /**Fixed data */ + space_usage: number; + /**Fixed data - If `catalog_type` is Column then these are columns flags */ + flags: number; + /**Fixed data */ + pages_or_locale: number; + /**Fixed data */ + root_flag: number; + /**Fixed data */ + record_offset: number; + /**Fixed data */ + lc_map_flags: number; + /**Fixed data */ + key_most: number; + /**Fixed data */ + lv_chunk_max: number; + /**Variable data */ + name: string; + /**Variable data */ + stats: Uint8Array; + /**Variable data */ + template_table: string; + /**Variable data */ + default_value: Uint8Array; + /**Variable data */ + key_fld_ids: Uint8Array; + /**Variable data */ + var_seg_mac: Uint8Array; + /**Variable data */ + conditional_columns: Uint8Array; + /**Variable data */ + tuple_limits: Uint8Array; + /**Variable data */ + version: Uint8Array; + /**Variable data */ + sort_id: Uint8Array; + /**Tagged data */ + callback_data: Uint8Array; + /**Tagged data */ + callback_dependencies: Uint8Array; + /**Tagged data */ + separate_lv: Uint8Array; + /**Tagged data */ + space_hints: Uint8Array; + /**Tagged data */ + space_deferred_lv_hints: Uint8Array; + /**Tagged data */ + local_name: Uint8Array; +} + +export enum CatalogType { + Table = "Table", + Column = "Column", + Index = "Index", + LongValue = "LongValue", + Callback = "Callback", + SlvAvail = "SlvAvail", + SlvSpaceMap = "SlvSpaceMap", + Unknown = "Unknown", +} + +export interface TableInfo { + obj_id_table: number; + table_page: number; + table_name: string; + column_info: ColumnInfo[]; + long_value_page: number; +} + +export interface ColumnInfo { + column_type: ColumnType; + column_name: string; + column_data: number[]; + column_id: number; + column_flags: ColumnFlags[]; + column_space_usage: number; + column_tagged_flags: TaggedDataFlag[]; +} + +export enum ColumnFlags { + NotNull = "NotNull", + Version = "Version", + AutoIncrement = "AutoIncrement", + MultiValued = "MultiValued", + Default = "Default", + EscrowUpdate = "EscrowUpdate", + Finalize = "Finalize", + UserDefinedDefault = "UserDefinedDefault", + TemplateColumnESE98 = "TemplateColumnESE98", + DeleteOnZero = "DeleteOnZero", + PrimaryIndexPlaceholder = "PrimaryIndexPlaceholder", + Compressed = "Compressed", + Encrypted = "Encrypted", + Versioned = "Versioned", + Deleted = "Deleted", + VersionedAdd = "VersionedAdd", +} + +enum TaggedDataFlag { + Variable = "Variable", + Compressed = "Compressed", + LongValue = "LongValue", + MultiValue = "MultiValue", + MultiValueSizeDefinition = "MultiValueSizeDefinition", Unknown = "Unknown", } ``` diff --git a/mod.ts b/mod.ts index 8754da1a..d02352bd 100644 --- a/mod.ts +++ b/mod.ts @@ -28,7 +28,6 @@ export { getMacho } from "./src/macos/macho.ts"; export { getPlist } from "./src/macos/plist.ts"; export { getUnifiedLog, - setupUnifiedLogParser, } from "./src/macos/unifiedlogs.ts"; export { getSafariDownloads, @@ -140,7 +139,4 @@ export { getRecycleBin, getRecycleBinFile } from "./src/windows/recyclebin.ts"; export { getChocolateyInfo } from "./src/windows/chocolatey.ts"; export { logonsWindows } from "./src/windows/eventlogs/logons.ts"; export { getShellItem } from "./src/windows/shellitems.ts"; -export { userAccessLog } from "./src/windows/ese/ual.ts"; -export { parseTable } from "./src/windows/ese.ts"; -export { updateHistory } from "./src/windows/ese/updates.ts"; export { getWmiPersist } from "./src/windows/wmi.ts"; diff --git a/src/windows/ese.ts b/src/windows/ese.ts index 9e94ad37..15b1d79c 100644 --- a/src/windows/ese.ts +++ b/src/windows/ese.ts @@ -22,7 +22,7 @@ export class EseDatabase { /** * Function to extract the Catalog from an ESE database - * @returns Array of `Catalog` entries + * @returns Array of `Catalog` entries or `WindowsError` */ public catalogInfo(): Catalog[] | WindowsError { try { @@ -114,13 +114,11 @@ export class EseDatabase { * Function to extract rows from ESE database table * @param pages Array of pages to use to get ESE rows * @param info `TableInfo` object - * @param name Name of table to parse * @returns HashMap of table data `Record` */ public getRows( pages: number[], info: TableInfo, - name: string, ): Record | WindowsError { try { //@ts-ignore: Custom Artemis function @@ -128,7 +126,7 @@ export class EseDatabase { this.path, pages, JSON.stringify(info), - name, + info.table_name, ); const results: Record = JSON.parse(data); @@ -145,7 +143,6 @@ export class EseDatabase { * Function to extract and filter rows from ESE database table. Useful if you want to combine tables based on shared key or you want to search for something * @param pages Array of pages to use to get ESE rows * @param info `TableInfo` object - * @param name Name of table to parse * @param column_name Name of column to filter on * @param column_data HashMap of column values to filter on `Record`. Only the key matters for filtering * @returns @@ -153,7 +150,6 @@ export class EseDatabase { public getFilteredRows( pages: number[], info: TableInfo, - name: string, column_name: string, column_data: Record, ): Record | WindowsError { @@ -163,7 +159,7 @@ export class EseDatabase { this.path, pages, JSON.stringify(info), - name, + info.table_name, column_name, column_data, ); @@ -182,14 +178,12 @@ export class EseDatabase { * Function to dump specific columns from an ESE database table. Useful if you do **not** want all table columns provided from `getRows()` function. * @param pages Array of pages to use to get ESE rows * @param info `TableInfo` object - * @param name Name of table to parse * @param column_names Array of columns to parse * @returns */ public dumpTableColumns( pages: number[], info: TableInfo, - name: string, column_names: string[], ): Record | WindowsError { try { @@ -198,7 +192,7 @@ export class EseDatabase { this.path, pages, JSON.stringify(info), - name, + info.table_name, column_names, ); diff --git a/src/windows/ese/ual.ts b/src/windows/ese/ual.ts index 0d8994e4..c3d304e2 100644 --- a/src/windows/ese/ual.ts +++ b/src/windows/ese/ual.ts @@ -15,6 +15,7 @@ interface RoleIds { */ export class UserAccessLogging extends EseDatabase { private info: TableInfo; + /**Pages associated with the table */ pages: number[]; /** @@ -51,7 +52,7 @@ export class UserAccessLogging extends EseDatabase { `RoleIds info object not initialized property. Table name is empty`, ); } - const rows = this.getRows(pages, this.info, "ROLE_IDS"); + const rows = this.getRows(pages, this.info); if (rows instanceof WindowsError) { return rows; } @@ -77,7 +78,7 @@ export class UserAccessLogging extends EseDatabase { `Clients info object not initialized property. Table name is empty`, ); } - const rows = this.getRows(pages, this.info, "CLIENTS"); + const rows = this.getRows(pages, this.info); if (rows instanceof WindowsError) { return rows; } diff --git a/src/windows/ese/updates.ts b/src/windows/ese/updates.ts index d1b317c9..9c681231 100644 --- a/src/windows/ese/updates.ts +++ b/src/windows/ese/updates.ts @@ -59,7 +59,7 @@ export class Updates extends EseDatabase { `tbHistory info object not initialized property. Table name is empty`, ); } - const rows = this.getRows(pages, this.info, this.table); + const rows = this.getRows(pages, this.info); if (rows instanceof WindowsError) { return rows; } diff --git a/types/windows/ese.ts b/types/windows/ese.ts index 42558e0f..4a8f3bdc 100644 --- a/types/windows/ese.ts +++ b/types/windows/ese.ts @@ -1,4 +1,4 @@ -/** Generic Interface for dumpting ESE tables */ +/** Generic Interface for dumping ESE tables */ export interface EseTable { column_type: ColumnType; column_name: string; From a7758d9d7d1d3d857a3b0a50b5de2a68a874ad69 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 1 Jun 2024 19:15:17 -0400 Subject: [PATCH 5/8] updates --- artemis-docs/docs/API/API Scenarios/ese.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artemis-docs/docs/API/API Scenarios/ese.md b/artemis-docs/docs/API/API Scenarios/ese.md index 4b5f9b13..0ec84ecb 100644 --- a/artemis-docs/docs/API/API Scenarios/ese.md +++ b/artemis-docs/docs/API/API Scenarios/ese.md @@ -14,7 +14,7 @@ However, these database files may become very large. For example, the Windows Se So we must careful that we do not read all of the data into memory. -Artemis provides a TypeScript EseDatabase class to help us parse and interact with ESE databases and giving us flexibility on much systems resources we can use. +Artemis provides a TypeScript EseDatabase class to help us parse and interact with ESE databases. ## ESE Parsing Guide From d8ac450f3558952a375fcd0dec84c3cd98205329 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 1 Jun 2024 19:17:18 -0400 Subject: [PATCH 6/8] updates --- artemis-docs/docs/API/API Scenarios/ese.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/artemis-docs/docs/API/API Scenarios/ese.md b/artemis-docs/docs/API/API Scenarios/ese.md index 0ec84ecb..ea1705f9 100644 --- a/artemis-docs/docs/API/API Scenarios/ese.md +++ b/artemis-docs/docs/API/API Scenarios/ese.md @@ -18,8 +18,8 @@ Artemis provides a TypeScript EseDatabase class to help us parse and interact wi ## ESE Parsing Guide -Let walkthrough a scenario on we can leverage the API to extract data from the Windows [User Access Logging database](https://www.crowdstrike.com/blog/user-access-logging-ual-overview/) (UAL). -The guide below assumes you have cloned the artemis API repository to your local system. You may also import the API remotely. +Let walkthrough a scenario where we can leverage the artemis API to extract data from the Windows [User Access Logging database](https://www.crowdstrike.com/blog/user-access-logging-ual-overview/) (UAL). +The guide below assumes you have cloned the artemis API repository to your local system. However, you may also import the API remotely, you will just need to update the imports. The functions in this guide are documented [here](../Artifacts/windows.md#ese-database-class) From 65510cca15a730878fac7d78f561572e13bc155e Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 1 Jun 2024 19:19:41 -0400 Subject: [PATCH 7/8] updates --- artemis-docs/docs/API/API Scenarios/ese.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/artemis-docs/docs/API/API Scenarios/ese.md b/artemis-docs/docs/API/API Scenarios/ese.md index ea1705f9..b54ca877 100644 --- a/artemis-docs/docs/API/API Scenarios/ese.md +++ b/artemis-docs/docs/API/API Scenarios/ese.md @@ -44,7 +44,7 @@ The above code initializes a new EseDatabase instance that we will use to parse ### Extract the Catalog -Before we can do any parsing of the ESE database must get the Catalog associated with the ESE database. +Before we can do any parsing of the database we must get the Catalog. The Catalog is a special table in all ESE databases that contains metadata on all tables and columns in the database. There are 4 high level steps required in order to extract data from an ESE database: @@ -140,7 +140,7 @@ Do not worry too much about the large amount of objects, artemis will handle all ### Getting Table information -Since we are parsing the Current.mdb database, we are mainly interesting the CLIENTS table. +Since we are parsing the Current.mdb database, we are mainly interested the CLIENTS table. The code below shows how to extract metadata associated with the CLIENTS table. @@ -204,7 +204,7 @@ function main() { main(); ``` -The code above will now also get all of the pages associated with the table CLIENTS! +The code above will now get all of the pages associated with the table CLIENTS! ### Getting our data From 37046b641dcd3bb349aa48852c8c9c10c5ce6e64 Mon Sep 17 00:00:00 2001 From: puffyCid <16283453+puffyCid@users.noreply.github.com> Date: Sat, 1 Jun 2024 22:52:59 -0400 Subject: [PATCH 8/8] fix for macos alias format. Migrating timestamps to ISO8601 --- artemis-docs/docs/Artifacts/macOS Artifacts/alias.md | 4 ++-- .../docs/Artifacts/macOS Artifacts/homebrew.md | 4 ++-- artemis-docs/docs/Artifacts/macOS Artifacts/munki.md | 4 ++-- .../docs/Artifacts/macOS Artifacts/quarantine.md | 2 +- artemis-docs/docs/Artifacts/macOS Artifacts/tcc.md | 4 ++-- src/macos/alias.ts | 8 ++++---- src/macos/homebrew.ts | 11 +++++++---- src/macos/plist/firewall.ts | 4 ++-- src/macos/sqlite/munki.ts | 3 ++- src/macos/sqlite/quarantine.ts | 6 +++--- src/macos/sqlite/tcc.ts | 5 +++-- src/timesketch/artifacts/macos/homebrew.ts | 7 +++---- types/macos/alias.ts | 4 ++-- types/macos/homebrew.ts | 4 ++-- types/macos/plist/xprotect.ts | 2 +- types/macos/sqlite/munki.ts | 4 ++-- types/macos/sqlite/quarantine.ts | 2 +- types/macos/sqlite/tcc.ts | 4 ++-- 18 files changed, 43 insertions(+), 39 deletions(-) diff --git a/artemis-docs/docs/Artifacts/macOS Artifacts/alias.md b/artemis-docs/docs/Artifacts/macOS Artifacts/alias.md index e351d6a4..cdadd3f6 100644 --- a/artemis-docs/docs/Artifacts/macOS Artifacts/alias.md +++ b/artemis-docs/docs/Artifacts/macOS Artifacts/alias.md @@ -38,13 +38,13 @@ An `alias` object structure export interface Alias { kind: string; volume_name: string; - volume_created: number; + volume_created: string; filesystem_type: number; disk_type: number; cnid: number; target_name: string; target_cnid: number; - target_created: number; + target_created: string; target_creator_code: number; target_type_code: number; number_directory_levels_from_alias_to_root: number; diff --git a/artemis-docs/docs/Artifacts/macOS Artifacts/homebrew.md b/artemis-docs/docs/Artifacts/macOS Artifacts/homebrew.md index bd016ec5..1c420849 100644 --- a/artemis-docs/docs/Artifacts/macOS Artifacts/homebrew.md +++ b/artemis-docs/docs/Artifacts/macOS Artifacts/homebrew.md @@ -52,8 +52,8 @@ A `HomebrewData` object structure export interface HomebrewReceipt extends HomebrewFormula { installedAsDependency: boolean; installedOnRequest: boolean; - installTime: number; - sourceModified: number; + installTime: string; + sourceModified: string; name: string; } diff --git a/artemis-docs/docs/Artifacts/macOS Artifacts/munki.md b/artemis-docs/docs/Artifacts/macOS Artifacts/munki.md index 52e95865..f9d8cb1b 100644 --- a/artemis-docs/docs/Artifacts/macOS Artifacts/munki.md +++ b/artemis-docs/docs/Artifacts/macOS Artifacts/munki.md @@ -53,8 +53,8 @@ export interface MunkiApplicationUsage { app_version: string; /**Path the application */ app_path: string; - /**Last time of the event in UNIXEPOCH seconds */ - last_time: number; + /**Last time of the event */ + last_time: string; /**Number of times of the event */ number_times: number; } diff --git a/artemis-docs/docs/Artifacts/macOS Artifacts/quarantine.md b/artemis-docs/docs/Artifacts/macOS Artifacts/quarantine.md index d16d6a7d..7b99171c 100644 --- a/artemis-docs/docs/Artifacts/macOS Artifacts/quarantine.md +++ b/artemis-docs/docs/Artifacts/macOS Artifacts/quarantine.md @@ -40,7 +40,7 @@ export interface MacosQuarantine { } export interface QuarantineEvent { id: string; - timestamp: number; + timestamp: string; bundle_id?: string; agent_name: string; url_string?: string; diff --git a/artemis-docs/docs/Artifacts/macOS Artifacts/tcc.md b/artemis-docs/docs/Artifacts/macOS Artifacts/tcc.md index e9d093c4..24f37bf0 100644 --- a/artemis-docs/docs/Artifacts/macOS Artifacts/tcc.md +++ b/artemis-docs/docs/Artifacts/macOS Artifacts/tcc.md @@ -61,11 +61,11 @@ export interface TccData { indirect_object_identifier: string; indirect_object_code_identity: SingleRequirement | undefined; flags: number | undefined; - last_modified: number; + last_modified: string; pid: number | undefined; pid_version: number | undefined; boot_uuid: string; - last_reminded: number; + last_reminded: string; } export enum Reason { diff --git a/src/macos/alias.ts b/src/macos/alias.ts index bd151f64..fe4535ed 100644 --- a/src/macos/alias.ts +++ b/src/macos/alias.ts @@ -11,7 +11,7 @@ import { nomUnsignedTwoBytes, } from "../nom/helpers.ts"; import { take } from "../nom/parsers.ts"; -import { hfsToUnixEpoch } from "../time/conversion.ts"; +import { hfsToUnixEpoch, unixEpochToISO } from "../time/conversion.ts"; import { MacosError } from "./errors.ts"; /** @@ -88,7 +88,7 @@ export function parseAlias(data: Uint8Array): Alias | MacosError { // Get the volume name const volume_name = extractUtf8String(string_data.nommed as Uint8Array); - const created_data = nomSignedFourBytes( + const created_data = nomUnsignedFourBytes( alias_data.remaining as Uint8Array, Endian.Be, ); @@ -257,13 +257,13 @@ export function parseAlias(data: Uint8Array): Alias | MacosError { const alias: Alias = { kind, volume_name, - volume_created: hfsToUnixEpoch(volume_created), + volume_created: unixEpochToISO(hfsToUnixEpoch(volume_created)), filesystem_type, disk_type, cnid, target_name, target_cnid, - target_created: hfsToUnixEpoch(target_created), + target_created: unixEpochToISO(hfsToUnixEpoch(target_created)), target_creator_code, target_type_code, number_directory_levels_from_alias_to_root, diff --git a/src/macos/homebrew.ts b/src/macos/homebrew.ts index 63c14684..208cbec2 100644 --- a/src/macos/homebrew.ts +++ b/src/macos/homebrew.ts @@ -5,6 +5,7 @@ import { } from "../../types/macos/homebrew.ts"; import { FileError } from "../filesystem/errors.ts"; import { glob, readTextFile } from "../filesystem/files.ts"; +import { unixEpochToISO } from "../time/conversion.ts"; /** * Function to get Homebrew info on installed packages and Casks @@ -53,8 +54,8 @@ export function getPackages(glob_path?: string): HomebrewReceipt[] { const brew_info: HomebrewReceipt = { installedAsDependency: false, installedOnRequest: false, - installTime: 0, - sourceModified: 0, + installTime: "", + sourceModified: "", version: "", name: "", description: "", @@ -108,11 +109,13 @@ export function getPackages(glob_path?: string): HomebrewReceipt[] { } const receipt_data = JSON.parse(receipt); - brew_info.installTime = receipt_data["time"]; + brew_info.installTime = unixEpochToISO(receipt_data["time"]); brew_info.installedAsDependency = receipt_data["installed_as_dependency"]; brew_info.installedOnRequest = receipt_data["installed_on_request"]; - brew_info.sourceModified = receipt_data["source_modified_time"]; + brew_info.sourceModified = unixEpochToISO( + receipt_data["source_modified_time"], + ); } brew_receipts.push(brew_info); diff --git a/src/macos/plist/firewall.ts b/src/macos/plist/firewall.ts index ae1e3eda..90f5eaeb 100644 --- a/src/macos/plist/firewall.ts +++ b/src/macos/plist/firewall.ts @@ -92,13 +92,13 @@ function parseApplications( application_info: { kind: "", volume_name: "", - volume_created: 0, + volume_created: "", filesystem_type: 0, disk_type: 0, cnid: 0, target_name: "", target_cnid: 0, - target_created: 0, + target_created: "", target_creator_code: 0, target_type_code: 0, number_directory_levels_from_alias_to_root: 0, diff --git a/src/macos/sqlite/munki.ts b/src/macos/sqlite/munki.ts index a6a51829..afba1c2a 100644 --- a/src/macos/sqlite/munki.ts +++ b/src/macos/sqlite/munki.ts @@ -2,6 +2,7 @@ import { ApplicationError } from "../../applications/errors.ts"; import { querySqlite } from "../../applications/sqlite.ts"; import { MacosError } from "../errors.ts"; import { MunkiApplicationUsage } from "../../../types/macos/sqlite/munki.ts"; +import { unixEpochToISO } from "../../time/conversion.ts"; /** * Function to extract application usage info from Munki database @@ -24,7 +25,7 @@ export function munkiApplicationUsage( bundle_id: value["bundle_id"] as string, app_version: value["app_version"] as string, app_path: value["app_path"] as string, - last_time: value["last_time"] as number, + last_time: unixEpochToISO(value["last_time"] as number), number_times: value["number_times"] as number, }; diff --git a/src/macos/sqlite/quarantine.ts b/src/macos/sqlite/quarantine.ts index fbc69e23..80ac40b7 100644 --- a/src/macos/sqlite/quarantine.ts +++ b/src/macos/sqlite/quarantine.ts @@ -8,7 +8,7 @@ import { QuarantineEvent, QuarantineType, } from "../../../types/macos/sqlite/quarantine.ts"; -import { cocoatimeToUnixEpoch } from "../../time/conversion.ts"; +import { cocoatimeToUnixEpoch, unixEpochToISO } from "../../time/conversion.ts"; /** * Function to extract macOS Quarantine Events @@ -53,9 +53,9 @@ export function quarantineEvents( for (const value of results) { const entry: QuarantineEvent = { id: value["LSQuarantineEventIdentifier"] as string, - timestamp: cocoatimeToUnixEpoch( + timestamp: unixEpochToISO(cocoatimeToUnixEpoch( value["LSQuarantineTimeStamp"] as number, - ), + )), agent_name: value["LSQuarantineAgentName"] as string, type: quarantineType(value["LSQuarantineTypeNumber"] as number), bundle_id: diff --git a/src/macos/sqlite/tcc.ts b/src/macos/sqlite/tcc.ts index 0e597109..45c4d6a7 100644 --- a/src/macos/sqlite/tcc.ts +++ b/src/macos/sqlite/tcc.ts @@ -15,6 +15,7 @@ import { decode } from "../../encoding/base64.ts"; import { EncodingError } from "../../encoding/errors.ts"; import { parseRequirementBlob } from "../codesigning/blob.ts"; import { SigningError } from "../codesigning/errors.ts"; +import { unixEpochToISO } from "../../time/conversion.ts"; /** * Query all `TCC.db` files on the system. `TCC.db` contains granted permissions for applications. @@ -82,11 +83,11 @@ function getTccData(data: Record[], path: string): TccValues { indirect_object_identifier: entry["indirect_object_identifier"] as string, indirect_object_code_identity: undefined, flags: entry["flags"] as number | undefined, - last_modified: entry["last_modified"] as number, + last_modified: unixEpochToISO(entry["last_modified"] as number), pid: entry["pid"] as number | undefined, pid_version: entry["pid_version"] as number | undefined, boot_uuid: entry["boot_uuid"] as string, - last_reminded: entry["last_reminded"] as number, + last_reminded: unixEpochToISO(entry["last_reminded"] as number), }; if (entry["csreq"] != undefined) { diff --git a/src/timesketch/artifacts/macos/homebrew.ts b/src/timesketch/artifacts/macos/homebrew.ts index 33937da1..6c045b50 100644 --- a/src/timesketch/artifacts/macos/homebrew.ts +++ b/src/timesketch/artifacts/macos/homebrew.ts @@ -1,6 +1,5 @@ import { HomebrewReceipt } from "../../../../types/macos/homebrew.ts"; import { TimesketchTimeline } from "../../../../types/timesketch/timeline.ts"; -import { unixEpochToISO } from "../../../time/conversion.ts"; /** * Function to timeline Homebrew Packages info @@ -14,7 +13,7 @@ export function timelineHomebrew( for (const item of data) { let entry: TimesketchTimeline = { - datetime: unixEpochToISO(item.installTime), + datetime: item.installTime, timestamp_desc: "Homebrew Package Installed", message: `${item.name} - ${item.description}`, data_type: "macos:homebrew:package", @@ -22,8 +21,8 @@ export function timelineHomebrew( }; entry = { ...entry, ...item }; - entry["installTime"] = unixEpochToISO(item.installTime); - entry["sourceModified"] = unixEpochToISO(item.sourceModified); + entry["installTime"] = item.installTime; + entry["sourceModified"] = item.sourceModified; entries.push(entry); } diff --git a/types/macos/alias.ts b/types/macos/alias.ts index 8fe95515..f47f1b19 100644 --- a/types/macos/alias.ts +++ b/types/macos/alias.ts @@ -11,13 +11,13 @@ export interface Alias { kind: string; volume_name: string; - volume_created: number; + volume_created: string; filesystem_type: number; disk_type: number; cnid: number; target_name: string; target_cnid: number; - target_created: number; + target_created: string; target_creator_code: number; target_type_code: number; number_directory_levels_from_alias_to_root: number; diff --git a/types/macos/homebrew.ts b/types/macos/homebrew.ts index d9dd0e4f..245b5379 100644 --- a/types/macos/homebrew.ts +++ b/types/macos/homebrew.ts @@ -1,8 +1,8 @@ export interface HomebrewReceipt extends HomebrewFormula { installedAsDependency: boolean; installedOnRequest: boolean; - installTime: number; - sourceModified: number; + installTime: string; + sourceModified: string; name: string; } diff --git a/types/macos/plist/xprotect.ts b/types/macos/plist/xprotect.ts index af15df05..21e612a1 100644 --- a/types/macos/plist/xprotect.ts +++ b/types/macos/plist/xprotect.ts @@ -5,7 +5,7 @@ export interface XprotectEntries { } export interface MatchData { - /**Hex encoded values. These are maybe compiled? Yara Rules */ + /**Hex encoded values */ pattern: string; filetype: string; sha1: string; diff --git a/types/macos/sqlite/munki.ts b/types/macos/sqlite/munki.ts index a5ea3190..a9318740 100644 --- a/types/macos/sqlite/munki.ts +++ b/types/macos/sqlite/munki.ts @@ -10,8 +10,8 @@ export interface MunkiApplicationUsage { app_version: string; /**Path the application */ app_path: string; - /**Last time of the event in UNIXEPOCH seconds */ - last_time: number; + /**Last time of the event */ + last_time: string; /**Number of times of the event */ number_times: number; } diff --git a/types/macos/sqlite/quarantine.ts b/types/macos/sqlite/quarantine.ts index 0ac35c09..b2bfdad2 100644 --- a/types/macos/sqlite/quarantine.ts +++ b/types/macos/sqlite/quarantine.ts @@ -4,7 +4,7 @@ export interface MacosQuarantine { } export interface QuarantineEvent { id: string; - timestamp: number; + timestamp: string; bundle_id?: string; agent_name: string; url_string?: string; diff --git a/types/macos/sqlite/tcc.ts b/types/macos/sqlite/tcc.ts index 063cdb5c..8a040f3e 100644 --- a/types/macos/sqlite/tcc.ts +++ b/types/macos/sqlite/tcc.ts @@ -18,11 +18,11 @@ export interface TccData { indirect_object_identifier: string; indirect_object_code_identity: SingleRequirement | undefined; flags: number | undefined; - last_modified: number; + last_modified: string; pid: number | undefined; pid_version: number | undefined; boot_uuid: string; - last_reminded: number; + last_reminded: string; } export enum Reason {