Selaa lähdekoodia

Merge pull request #45 from sealibora/master

Update third party reporters to v4, centralize timestamp parsing.
Rich Infante 6 vuotta sitten
vanhempi
commit
67df3c9b67
30 muutettua tiedostoa jossa 518 lisäystä ja 560 poistoa
  1. 52 0
      tools/backup.js
  2. 1 1
      tools/cli.js
  3. 4 0
      tools/reports/calendar/events.js
  4. 3 2
      tools/reports/messages/conversations.js
  5. 6 6
      tools/reports/messages/messages.js
  6. 4 3
      tools/reports/notes/notes.js
  7. 60 7
      tools/reports/phone/address_book.js
  8. 2 1
      tools/reports/phone/calls.js
  9. 2 1
      tools/reports/photos/locations.js
  10. 3 2
      tools/reports/safari/open_tabs.js
  11. 2 1
      tools/reports/safari/webhistory.js
  12. 2 1
      tools/reports/system/geofences.js
  13. 40 77
      tools/reports/thirdparty/facebook/messenger.js
  14. 15 22
      tools/reports/thirdparty/facebook/profile.js
  15. 14 19
      tools/reports/thirdparty/gmail/accounts.js
  16. 14 19
      tools/reports/thirdparty/gmail/shared_contacts.js
  17. 16 22
      tools/reports/thirdparty/instagram/fb_friends.js
  18. 14 22
      tools/reports/thirdparty/instagram/following_users_coded.js
  19. 15 22
      tools/reports/thirdparty/instagram/profile.js
  20. 16 22
      tools/reports/thirdparty/instagram/recent_searches.js
  21. 38 52
      tools/reports/thirdparty/skype/accounts.js
  22. 34 48
      tools/reports/thirdparty/skype/calls.js
  23. 16 22
      tools/reports/thirdparty/spotify/searches.js
  24. 27 35
      tools/reports/thirdparty/viber/calls.js
  25. 20 30
      tools/reports/thirdparty/viber/contacts.js
  26. 21 29
      tools/reports/thirdparty/viber/messages.js
  27. 25 36
      tools/reports/thirdparty/waze/favorites.js
  28. 20 25
      tools/reports/thirdparty/waze/places.js
  29. 24 33
      tools/reports/thirdparty/waze/recents.js
  30. 8 0
      tools/util/apple_timestamp.js

+ 52 - 0
tools/backup.js

@@ -3,6 +3,7 @@ const sqlite3 = require('sqlite3')
 const path = require('path')
 const log = require('./util/log')
 const filehash = require('./util/backup_filehash')
+const manifestMBDBParse = require('./util/manifest_mbdb_parse')
 
 /**
  * Backup3 is the version 4 of the backup library.
@@ -100,6 +101,57 @@ class Backup {
     throw new Error(`Could not find a file needed for this report. It may not be compatibile with this specific backup or iOS Version.`)
   }
 
+
+    /// Get the manifest for an sqlite database if available
+    getSqliteFileManifest () {
+        return new Promise(async (resolve, reject) => {
+            this.openDatabase('Manifest.db', true)
+                .then(db => {
+                    db.all('SELECT fileID, domain, relativePath as filename from FILES', async function (err, rows) {
+                        if (err) reject(err)
+                        
+                        resolve(rows)
+                    })
+                })
+                .catch(reject)
+        })
+    }
+
+    /// Get the manifest from the mbdb file
+    getMBDBFileManifest () {
+        return new Promise((resolve, reject) => {
+            let mbdbPath = this.getFileName('Manifest.mbdb', true)
+            manifestMBDBParse.process(mbdbPath, resolve, reject)
+        })
+    }
+    
+    /// Try to load both of the manifest files
+    getManifest () {
+        return new Promise(async (resolve, reject) => {
+            // Try the new sqlite file database.
+            try {
+                log.verbose('Trying sqlite manifest...')
+                let item = await this.getSqliteFileManifest(this)
+                return resolve(item)
+            } catch (e) {
+                log.verbose('Trying sqlite manifest... [failed]', e)
+            }
+            
+            // Try the mbdb file database
+            try {
+                log.verbose('Trying mbdb manifest...')
+                let item = await this.getMBDBFileManifest(this)
+                return resolve(item)
+            } catch (e) {
+                log.verbose('Trying mbdb manifest...[failed]', e)
+            }
+            
+            reject(new Error('Could not find a manifest.'))
+        })
+    }
+
+    
+
   /**
    * Open a database referenced by a fileID
    * It uses getFileName(), so it tries both v2 and v3 paths.

+ 1 - 1
tools/cli.js

@@ -33,7 +33,7 @@ program
   .option('-r, --report <report_type>', 'Select a report type. see below for a full list.')
   .option('-i, --id <id>', 'Specify an ID for filtering certain reports')
   .option('-f, --formatter <type>', 'Specify output format. default: table')
-  .option(`-e, --extract <dir>`, 'Extract data for commands. supported by: voicemail-files, manifest')
+  .option(`-e, --extract <dir>`, 'Extract data for commands. supported by: voicemail-files, manifest, addressbook')
   .option('-o, --output <path>', 'Specify an output directory for files to be written to.')
   .option(`-v, --verbose`, 'Verbose debugging output')
   .option(`    --plugins <plugins>`, 'List of pluging modules to use')

+ 4 - 0
tools/reports/calendar/events.js

@@ -1,5 +1,6 @@
 // Derive filenames based on domain + file path
 const fileHash = require('../../util/backup_filehash')
+const apple_timestamp = require('../../util/apple_timestamp')
 
 const CAL_DB = fileHash('Library/Calendar/Calendar.sqlitedb')
 
@@ -17,6 +18,7 @@ module.exports = {
   // Fields for apps report
   output: {
     timestamp: el => (new Date((el.start_date + 978307200) * 1000).toDateString()) + ' ' + (new Date((el.start_date + 978307200) * 1000).toTimeString()),
+    timestamp_string: el => el.start_date_string,  
     title: el => el.summary,
     content: el => el.description,
     calendarId: el => el.calendar_id,
@@ -31,6 +33,8 @@ function calendarReport (backup) {
         const query = `
         SELECT
           CalendarItem.*,
+          ${apple_timestamp.parse('CalendarItem.start_date')} AS start_date_string,
+
           Calendar.title as calendar_title
         FROM CalendarItem
         LEFT JOIN Calendar ON

+ 3 - 2
tools/reports/messages/conversations.js

@@ -2,6 +2,7 @@ const bplist = require('bplist-parser')
 
 const fileHash = require('../../util/backup_filehash')
 const log = require('../../util/log')
+const apple_timestamp = require('../../util/apple_timestamp')
 
 const SMS_DB = fileHash('Library/SMS/sms.db')
 
@@ -74,10 +75,10 @@ function getConversationsiOS9 (backup) {
 }
 
 function getConversationsiOS10iOS11 (backup) {
-  return new Promise((resolve, reject) => {
+    return new Promise((resolve, reject) => {
     backup.openDatabase(SMS_DB)
       .then(db => {
-        db.all(`SELECT *, datetime(last_read_message_timestamp / 1000000000 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM chat ORDER BY last_read_message_timestamp ASC`, async function (err, rows) {
+        db.all(`SELECT *, ${apple_timestamp.parse('last_read_message_timestamp')} AS XFORMATTEDDATESTRING FROM chat ORDER BY last_read_message_timestamp ASC`, async function (err, rows) {
           if (err) return reject(err)
           rows = rows || []
 

+ 6 - 6
tools/reports/messages/messages.js

@@ -1,6 +1,6 @@
 const fileHash = require('../../util/backup_filehash')
 const log = require('../../util/log')
-
+const apple_timestamp = require('../../util/apple_timestamp')
 const SMS_DB = fileHash('Library/SMS/sms.db')
 
 module.exports = {
@@ -23,7 +23,7 @@ module.exports = {
   // Available fields.
   output: {
     id: el => el.ROWID,
-    date: el => el.XFORMATTEDDATESTRING,
+    date: el => el.date,
     sender: el => el.x_sender,
     text: el => (el.text || '').trim(),
     dateRead: el => el.date_read + '',
@@ -65,7 +65,7 @@ function getMessagesiOS9 (backup, chatId) {
       SELECT 
         message.*,
         handle.id as sender_name,
-        datetime(date + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
+        ${apple_timestamp.parse(date)} AS date
       FROM chat_message_join 
       INNER JOIN message 
         ON message.rowid = chat_message_join.message_id 
@@ -106,9 +106,9 @@ function getMessagesiOS10iOS11 (backup, chatId) {
       SELECT 
         message.*,
         handle.id as sender_name,
-        datetime((date_read + 978307200), 'unixepoch') as date_read,
-        datetime((date_delivered + 978307200), 'unixepoch') as date_delivered,
-        datetime(date / 1000000000 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING
+        ${apple_timestamp.parse('date_read')} AS date_read,
+        ${apple_timestamp.parse('date_delivered')} AS date_delivered,
+        ${apple_timestamp.parse('date')} AS date
       FROM chat_message_join 
       INNER JOIN message 
         ON message.rowid = chat_message_join.message_id 

+ 4 - 3
tools/reports/notes/notes.js

@@ -1,5 +1,6 @@
 const fileHash = require('../../util/backup_filehash')
 const log = require('../../util/log')
+const apple_timestamp = require('../../util/apple_timestamp')
 
 const NOTES_DB = fileHash('Library/Notes/notes.sqlite')
 const NOTES2_DB = fileHash('NoteStore.sqlite', 'AppDomainGroup-group.com.apple.notes')
@@ -71,7 +72,7 @@ function getNewNotesiOS9 (backup) {
   return new Promise((resolve, reject) => {
     backup.openDatabase(NOTES2_DB)
       .then(db => {
-        db.all(`SELECT ZICCLOUDSYNCINGOBJECT.*, ZICNOTEDATA.ZDATA as X_CONTENT_DATA, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING FROM ZICCLOUDSYNCINGOBJECT LEFT JOIN ZICNOTEDATA ON ZICCLOUDSYNCINGOBJECT.ZNOTE = ZICNOTEDATA.ZNOTE`, async function (err, rows) {
+        db.all(`SELECT ZICCLOUDSYNCINGOBJECT.*, ZICNOTEDATA.ZDATA as X_CONTENT_DATA, ${apple_timestamp.parse('ZCREATIONDATE')} AS XFORMATTEDDATESTRING FROM ZICCLOUDSYNCINGOBJECT LEFT JOIN ZICNOTEDATA ON ZICCLOUDSYNCINGOBJECT.ZNOTE = ZICNOTEDATA.ZNOTE`, async function (err, rows) {
           if (err) reject(err)
 
           resolve(rows)
@@ -85,7 +86,7 @@ function getOldNotes (backup) {
   return new Promise((resolve, reject) => {
     backup.openDatabase(NOTES_DB)
       .then(db => {
-        db.all(`SELECT *, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZNOTE LEFT JOIN ZNOTEBODY ON ZBODY = ZNOTEBODY.Z_PK`, function (err, rows) {
+        db.all(`SELECT *, ${apple_timestamp.parse('ZCREATIONDATE')} AS XFORMATTEDDATESTRING from ZNOTE LEFT JOIN ZNOTEBODY ON ZBODY = ZNOTEBODY.Z_PK`, function (err, rows) {
           if (err) reject(err)
           resolve(rows)
         })
@@ -98,7 +99,7 @@ function getNewNotesiOS10iOS11 (backup) {
   return new Promise((resolve, reject) => {
     backup.openDatabase(NOTES2_DB)
       .then(db => {
-        db.all(`SELECT ZICCLOUDSYNCINGOBJECT.*, ZICNOTEDATA.ZDATA as X_CONTENT_DATA, datetime(ZCREATIONDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, datetime(ZCREATIONDATE1 + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING1 FROM ZICCLOUDSYNCINGOBJECT LEFT JOIN ZICNOTEDATA ON ZICCLOUDSYNCINGOBJECT.ZNOTE = ZICNOTEDATA.ZNOTE`, function (err, rows) {
+        db.all(`SELECT ZICCLOUDSYNCINGOBJECT.*, ZICNOTEDATA.ZDATA as X_CONTENT_DATA, ${apple_timestamp.parse('(ZCREATIONDATE')} AS XFORMATTEDDATESTRING, ${apple_timestamp.parse('ZCREATIONDATE1')} AS XFORMATTEDDATESTRING1 FROM ZICCLOUDSYNCINGOBJECT LEFT JOIN ZICNOTEDATA ON ZICCLOUDSYNCINGOBJECT.ZNOTE = ZICNOTEDATA.ZNOTE`, function (err, rows) {
           if (err) reject(err)
 
           resolve(rows)

+ 60 - 7
tools/reports/phone/address_book.js

@@ -1,3 +1,8 @@
+const fs = require('fs-extra')
+const path = require('path')
+const log = require('../../util/log')
+const apple_timestamp = require('../../util/apple_timestamp')
+
 module.exports = {
   version: 4,
   name: 'phone.address_book',
@@ -5,8 +10,16 @@ module.exports = {
   requiresBackup: true,
 
   // Run on a v3 lib / backup object.
-  run (lib, { backup }) {
-    return getAddressBook(backup)
+    run (lib, { backup, extract }) {
+    return new Promise(async (resolve, reject) => {
+      try {
+          let items = await getAddressBook(backup, extract)
+
+        resolve(items)
+      } catch (e) {
+        reject(e)
+      }
+    })
   },
 
   // Manifest fields.
@@ -18,14 +31,21 @@ module.exports = {
     phoneWork: el => el.phone_work || null,
     phoneMobile: el => el.phone_mobile || null,
     phoneHome: el => el.phone_home || null,
+    iphone: el => el.iphone || null,
     email: el => el.email || null,
     createdDate: el => el.created_date || null,
     note: el => el.note || null,
-    picture: el => !!el.profile_picture
+    picture: el => !!el.profile_picture,
+    picture_file: el =>  el.profile_picture_file || null,
+    // picture_base64: el => el.profile_picture,
+    services: el => el.services,
+    address: el => el.address,
+    city: el => el.city
+
   }
 }
 
-function getAddressBook (backup) {
+function getAddressBook (backup, extract) {
   return new Promise((resolve, reject) => {
     backup.openDatabase(backup.getFileID('Library/AddressBook/AddressBook.sqlitedb'))
       .then(db => {
@@ -39,11 +59,12 @@ function getAddressBook (backup) {
           , ABPerson.Department as department
           , ABPerson.Birthday as birthday
           , ABPerson.JobTitle as jobtitle
-          , datetime(ABPerson.CreationDate + 978307200, 'unixepoch') as created_date 
-          , datetime(ABPerson.ModificationDate + 978307200, 'unixepoch') as updated_date 
+          , ${apple_timestamp.parse('ABPerson.CreationDate')} as created_date 
+          , ${apple_timestamp.parse('ABPerson.ModificationDate')} as updated_date 
           , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Work>!$_')) as phone_work
           , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Mobile>!$_')) as phone_mobile
           , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = '_$!<Home>!$_')) as phone_home
+          , (select value from ABMultiValue where property = 3 and record_id = ABPerson.ROWID and label = (select ROWID from ABMultiValueLabel where value = 'iPhone')) as iphone
 
           , (select value from ABMultiValue where property = 4 and record_id = ABPerson.ROWID) as email
           
@@ -91,8 +112,20 @@ function getAddressBook (backup) {
                     ele.profile_picture = null
                     if (row) {
                       ele.profile_picture = (row.data || '').toString('base64')
+                        if (extract && row.data) {
+                            fs.ensureDir(path.dirname(extract)).then( (err) => {
+                                var outFile = path.join(extract, `profile_picture_${ele.ROWID}.jpg`)
+                
+                                // Log the export
+                                log.verbose('extract', outFile)
+                
+                                fs.writeFile(outFile, row.data, (err) => {
+                                    if (err) console.log(err);
+                                })
+                                ele.profile_picture_file = outFile                            
+                            })
+                        }
                     }
-
                     iterateElements(elements, index + 1, callback)
                   })
                 })
@@ -107,3 +140,23 @@ function getAddressBook (backup) {
       .catch(reject)
   })
 }
+
+function extractProfilePictures (items, extractDest) {
+    // Ensure output dir exists
+    fs.ensureDir(path.dirname(extractDest)).then( (err) => {
+        if (err) console.log(err)
+        for (let item of items) {
+            if (item.profile_picture) {
+                var outFile = path.join(extractDest, `profile_picture_${item.ROWID}.jpg`)
+                
+                // Log the export
+                log.verbose('extract', outFile)
+                
+                fs.writeFile(outFile, Buffer.from(item.profile_picture, 'base64'), (err) => {
+                    if (err) console.log(err);
+                })
+                item.profile_picture_file = outFile
+            }
+        }
+    })    
+}

+ 2 - 1
tools/reports/phone/calls.js

@@ -1,5 +1,6 @@
 const fileHash = require('../../util/backup_filehash')
 const log = require('../../util/log')
+const apple_timestamp = require('../../util/apple_timestamp')
 
 const CALLS_DB = '2b2b0084a1bc3a5ac8c27afdf14afb42c61a19ca'
 const CALLS2_DB = fileHash('Library/CallHistoryDB/CallHistory.storedata')
@@ -105,7 +106,7 @@ function getCallsListLater (backup) {
   return new Promise((resolve, reject) => {
     backup.openDatabase(CALLS2_DB)
       .then(db => {
-        db.all(`SELECT *, datetime(ZDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZCALLRECORD ORDER BY ZDATE ASC`, async function (err, rows) {
+        db.all(`SELECT *, ${apple_timestamp.parse('ZDATE')} AS XFORMATTEDDATESTRING from ZCALLRECORD ORDER BY ZDATE ASC`, async function (err, rows) {
           if (err) reject(err)
 
           resolve(rows)

+ 2 - 1
tools/reports/photos/locations.js

@@ -1,4 +1,5 @@
 const fileHash = require('../../util/backup_filehash')
+const apple_timestamp = require('../../util/apple_timestamp')
 
 const PHOTOS_DB = fileHash('Media/PhotoData/Photos.sqlite', 'CameraRollDomain')
 
@@ -31,7 +32,7 @@ function getPhotoLocationHistory (backup) {
           ZLATITUDE, 
           ZLONGITUDE,
           ZFILENAME,
-          datetime(ZDATECREATED + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING 
+          ${apple_timestamp.parse('ZDATECREATED')} AS XFORMATTEDDATESTRING 
           FROM ZGENERICASSET ORDER BY ZDATECREATED ASC`, function (err, rows) {
           if (err) reject(err)
 

+ 3 - 2
tools/reports/safari/open_tabs.js

@@ -1,4 +1,5 @@
 const fileHash = require('../../util/backup_filehash')
+const apple_timestamp = require('../../util/apple_timestamp')
 
 const TABS_DB = fileHash('Library/Safari/BrowserState.db', 'AppDomain-com.apple.mobilesafari')
 
@@ -17,7 +18,7 @@ module.exports = {
   output: {
     title: el => el.title,
     url: el => el.url,
-    lastViewedTime: el => (new Date((el.last_viewed_time + 978307200) * 1000).toDateString()) + ' ' + (new Date((el.last_viewed_time + 978307200) * 1000).toTimeString())
+    lastViewedTime: el => el.last_viewed
   }
 }
 
@@ -26,7 +27,7 @@ const openTabsReport = (backup) => {
     backup.openDatabase(TABS_DB)
       .then(db => {
         db.all(`
-          select * from tabs
+          select *, ${apple_timestamp.parse('last_viewed_time')} as last_viewed from tabs
           order by last_viewed_time DESC
           `, function (err, rows) {
           if (err) reject(err)

+ 2 - 1
tools/reports/safari/webhistory.js

@@ -1,5 +1,6 @@
 const { URL } = require('url')
 const fileHash = require('../../util/backup_filehash')
+const apple_timestamp = require('../../util/apple_timestamp')
 const HISTORY_DB = fileHash('Library/Safari/History.db', 'AppDomain-com.apple.mobilesafari')
 
 module.exports = {
@@ -27,7 +28,7 @@ function getWebHistory (backup) {
   return new Promise((resolve, reject) => {
     backup.openDatabase(HISTORY_DB)
       .then(db => {
-        db.all(`SELECT *, datetime(visit_time + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from history_visits LEFT JOIN history_items ON history_items.ROWID = history_visits.history_item`, function (err, rows) {
+        db.all(`SELECT *, ${apple_timestamp.parse('visit_time')} AS XFORMATTEDDATESTRING from history_visits LEFT JOIN history_items ON history_items.ROWID = history_visits.history_item`, function (err, rows) {
           if (err) reject(err)
 
           resolve(rows)

+ 2 - 1
tools/reports/system/geofences.js

@@ -1,4 +1,5 @@
 const fileHash = require('../../util/backup_filehash')
+const apple_timestamp = require('../../util/apple_timestamp')
 
 const GEO_DB = fileHash('Library/Caches/locationd/consolidated.db', 'RootDomain')
 
@@ -26,7 +27,7 @@ function getGeoFences (backup) {
   return new Promise((resolve, reject) => {
     backup.openDatabase(GEO_DB)
       .then(db => {
-        db.all(`SELECT datetime(Timestamp + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING, Latitude, Longitude, Distance FROM Fences ORDER BY Timestamp ASC`, async function (err, rows) {
+        db.all(`SELECT ${apple_timestamp.parse('Timestamp')} AS XFORMATTEDDATESTRING, Latitude, Longitude, Distance FROM Fences ORDER BY Timestamp ASC`, async function (err, rows) {
           if (err) reject(err)
 
           resolve(rows)

+ 40 - 77
tools/reports/thirdparty/facebook/messenger.js

@@ -5,27 +5,29 @@ const fileHash = require('../../../util/backup_filehash')
 
 const domain = 'AppDomainGroup-group.com.facebook.Messenger'
 
-module.exports.name = 'facebook_messenger_friends'
-module.exports.description = 'Show Facebook Messenger friends'
 
-// Specify this only works for iOS 9+
-module.exports.supportedVersions = '>=10.0'
 
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
+module.exports = {
+  version: 4,
+  name: 'facebook_messenger_friends',
+  description: `Show Facebook Messenger friends`,
+  requiresBackup: true,
 
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return facebookMessengerFriendsReport(backup)
+    },
 
-// You can also provide an array of functions instead of using `module.exports.func`.
-// These functions *should* be independent ranges to ensure reliable execution
-module.exports.functions = {
+  // Fields for apps report
+  output: {
+      'Facebook Friend Username': el => el.field_value
+  }
+}
 
-  '>=10.0': function (program, backup, resolve, reject) {
-    // This function would be called for iOS 10+
 
-    backup.getFileManifest()
+const facebookMessengerFriendsReport = (backup, file) => {
+  return new Promise((resolve, reject) => {
+    backup.getManifest()
       .then((items) => {
         let filename = 'fbomnistore.db'
         let fileitem = items.find((file) => {
@@ -35,68 +37,29 @@ module.exports.functions = {
         if (fileitem) {
           let filepath = fileitem.relativePath
           let file = fileHash(filepath, domain)
-          return facebookMessengerFriendsReport(backup, file)
-        } else return [] // Return an empty array to the formatter, since no fbomnistore.db file can be found in the manifest
-      })
-      .then((items) => {
-        var result = program.formatter.format(items, {
-          program: program,
-          columns: {
-            'Facebook Friend Usernames': el => el.field_value
-          }
-        })
-
-        resolve(result)
-      })
-      .catch((e) => {
-        log.error('[!] Encountered an Error:', e)
-      })
-  },
-
-  '>=5.0,<10.0': function (program, backup, resolve, reject) {
-    // This function would be called for all iOS 5 up to iOS 9.x.
-    // TODO
-    /* backup.getOldFileManifest()
-      .then((items) => {
-        var result = program.formatter.format(items, {
-          program: program,
-          columns: {
-            'Facebook Friend Username': el => el.field_value
-          }
-        })
-
-        resolve(result)
-      })
-      .catch(reject) */
-  }
-}
-
-const facebookMessengerFriendsReport = (backup, file) => {
-  return new Promise((resolve, reject) => {
-    var database = backup.getDatabase(file)
-    try {
-      database.get(`
-      SELECT name 
-      FROM sqlite_master 
-      WHERE type='table' 
-      AND name LIKE 'collection_index#messenger_contacts_ios%' 
-      LIMIT 1
-      `,
-      (err, tableName) => {
-        if (err) return reject(err)
-        tableName = tableName.name
-        log.verbose('Table', tableName)
-        database.all(`
-        SELECT field_value 
-        FROM '${tableName}' 
-        WHERE field_name='username'
-        `, (err, rows) => {
-          if (err) return reject(err)
-          resolve(rows)
-        })
+          backup.openDatabase(file).then(database => {
+              database.get(`
+                      SELECT name 
+                      FROM sqlite_master 
+                      WHERE type='table' 
+                      AND name LIKE 'collection_index#messenger_contacts_ios%' 
+                      LIMIT 1
+                      `,
+                     (err, tableName) => {
+                         if (err) return reject(err)
+                         tableName = tableName.name
+                         log.verbose('Table', tableName)
+                         database.all(`
+                            SELECT field_value 
+                            FROM '${tableName}' 
+                            WHERE field_name='username'
+                            `, (err, rows) => {
+                                if (err) return reject(err)
+                                resolve(rows)
+                            })
+                     })
+          }).catch(reject)
+        } else reject("Cannot find fbomnistore.db") // Return an empty array to the formatter, since no fbomnistore.db file can be found in the manifest
       })
-    } catch (e) {
-      reject(e)
-    }
   })
 }

+ 15 - 22
tools/reports/thirdparty/facebook/profile.js

@@ -6,29 +6,22 @@ const fileHash = require('../../../util/backup_filehash')
 
 const file = fileHash('Library/Preferences/com.facebook.Messenger.plist', 'AppDomain-com.facebook.Messenger')
 
-module.exports.name = 'facebook_profile'
-module.exports.description = 'Show Facebook Messenger user id'
-
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
-
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  facebookProfileReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
-          'Facebook User ID': el => el.fbid
-        }
-      })
 
-      resolve(result)
-    })
-    .catch(reject)
+module.exports = {
+  version: 4,
+  name: 'facebook_profile',
+  description: `Show Facebook Messenger user id`,
+  requiresBackup: true,
+
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return facebookProfileReport(backup)
+    },
+
+  // Fields for apps report
+  output: {
+          'Facebook User ID': el => el.fbid
+  }
 }
 
 const facebookProfileReport = (backup) => {

+ 14 - 19
tools/reports/thirdparty/gmail/accounts.js

@@ -6,33 +6,28 @@ const fileHash = require('../../../util/backup_filehash')
 
 const file = fileHash('Library/Preferences/group.com.google.Gmail.plist', 'AppDomainGroup-group.com.google.Gmail')
 
-module.exports.name = 'gmail_accounts'
-module.exports.description = 'Show Gmail account(s) information'
 
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
+module.exports = {
+  version: 4,
+  name: 'gmail_accounts',
+  description: `Show Gmail account(s) information`,
+  requiresBackup: true,
 
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return gmailAccountsReport(backup)
+    },
 
-module.exports.func = function (program, backup, resolve, reject) {
-  gmailAccountsReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+  // Fields for apps report
+  output: {
           'Id': el => el.id,
           'Email': el => el.email,
           'Avatar': el => el.avatar || null
-        }
-      })
-
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
+
+
 const gmailAccountsReport = (backup) => {
   return new Promise((resolve, reject) => {
     var filename = backup.getFileName(file)

+ 14 - 19
tools/reports/thirdparty/gmail/shared_contacts.js

@@ -6,34 +6,29 @@ const fileHash = require('../../../util/backup_filehash')
 
 const file = fileHash('Library/Preferences/group.com.google.Gmail.plist', 'AppDomainGroup-group.com.google.Gmail')
 
-module.exports.name = 'gmail_shared_contacts'
-module.exports.description = 'Show Gmail account(s) shared contacts information'
 
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
+module.exports = {
+  version: 4,
+  name: 'gmail_shared_contacts',
+  description: `Show Gmail account(s) shared contacts information`,
+  requiresBackup: true,
 
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return gmailAccountsReport(backup)
+    },
 
-module.exports.func = function (program, backup, resolve, reject) {
-  gmailAccountsReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+  // Fields for apps report
+  output: {
           'Account': el => el.account,
           'Name': el => el.name,
           'Email': el => el.email,
           'Avatar': el => el.avatar
-        }
-      })
-
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
+
+
 const gmailAccountsReport = (backup) => {
   return new Promise((resolve, reject) => {
     var filename = backup.getFileName(file)

+ 16 - 22
tools/reports/thirdparty/instagram/fb_friends.js

@@ -6,34 +6,28 @@ const fileHash = require('../../../util/backup_filehash')
 
 const file = fileHash('Library/Preferences/group.com.burbn.instagram.plist', 'AppDomainGroup-group.com.burbn.instagram')
 
-module.exports.name = 'instagram_fb_friends'
-module.exports.description = 'Show Instagram and Facebook friends data'
-
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
-
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  instagramRecentSearchesReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+
+module.exports = {
+  version: 4,
+  name: 'instagram_fb_friends',
+  description: `Show Instagram and Facebook friends data`,
+  requiresBackup: true,
+
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return instagramRecentSearchesReport(backup)
+    },
+
+  // Fields for apps report
+  output: {
           'Fb_id': el => el.fb_id,
           'Name': el => el.full_name,
           'Profile Pic': el => el.profile_pic_url,
           'Invited': el => el.is_invited
-        }
-      })
-
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
+
 const instagramRecentSearchesReport = (backup) => {
   return new Promise((resolve, reject) => {
     var results = []

+ 14 - 22
tools/reports/thirdparty/instagram/following_users_coded.js

@@ -6,29 +6,21 @@ const fileHash = require('../../../util/backup_filehash')
 
 const file = fileHash('Library/Preferences/group.com.burbn.instagram.plist', 'AppDomainGroup-group.com.burbn.instagram')
 
-module.exports.name = 'instagram_following_users_coded'
-module.exports.description = 'Show Instagram following users coded data'
-
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
-
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  instagramRecentSearchesReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+module.exports = {
+  version: 4,
+  name: 'instagram_following_users_coded',
+  description: `Show Instagram following users coded data`,
+  requiresBackup: true,
+
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return instagramRecentSearchesReport(backup)
+    },
+
+  // Fields for apps report
+  output: {
           'Identifier': el => el
-        }
-      })
-
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
 const instagramRecentSearchesReport = (backup) => {

+ 15 - 22
tools/reports/thirdparty/instagram/profile.js

@@ -6,32 +6,25 @@ const fileHash = require('../../../util/backup_filehash')
 
 const file = fileHash('Library/Preferences/com.burbn.instagram.plist', 'AppDomain-com.burbn.instagram')
 
-module.exports.name = 'instagram_profile'
-module.exports.description = 'Show Instagram profile/user data'
-
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
-
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  instagramProfileReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+module.exports = {
+  version: 4,
+  name: 'instagram_profile',
+  description: `Show Instagram profile/user data`,
+  requiresBackup: true,
+
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return instagramProfileReport(backup)
+    },
+
+  // Fields for apps report
+  output: {
           'Key': el => el.key,
           'Value': el => el.value
-        }
-      })
-
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
+
 function KeyValue (property, plist) {
   this.key = property
   this.value = plist[property] ? plist[property] : 'N/A'

+ 16 - 22
tools/reports/thirdparty/instagram/recent_searches.js

@@ -6,32 +6,26 @@ const fileHash = require('../../../util/backup_filehash')
 
 const file = fileHash('Library/Preferences/group.com.burbn.instagram.plist', 'AppDomainGroup-group.com.burbn.instagram')
 
-module.exports.name = 'instagram_recent_searches'
-module.exports.description = 'Show Instagram recent searches coded data'
-
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
-
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  instagramRecentSearchesReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+
+module.exports = {
+  version: 4,
+  name: 'instagram_recent_searches',
+  description: `Show Instagram recent searches coded data`,
+  requiresBackup: true,
+
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return instagramRecentSearchesReport(backup)
+    },
+
+  // Fields for apps report
+  output: {
           'Type': el => el.type,
           'Identifier': el => el.identifier
-        }
-      })
-
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
+
 const instagramRecentSearchesReport = (backup) => {
   return new Promise((resolve, reject) => {
     var results = []

+ 38 - 52
tools/reports/thirdparty/skype/accounts.js

@@ -3,61 +3,47 @@ const fileHash = require('../../../util/backup_filehash')
 
 const domain = 'AppDomain-com.skype.skype'
 
-module.exports.name = 'skype_accounts'
-module.exports.description = 'Show Skype accounts'
+module.exports = {
+  version: 4,
+  name: 'skype.accounts',
+  description: `Show Skype accounts`,
+  requiresBackup: true,
 
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return skypeAccountsReport(backup)
+    },
 
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  backup.getFileManifest()
-    .then((items) => {
-      let filename = 'main.db'
-      let fileitem = items.find((file) => {
-        if (file && file.relativePath) {
-          return ~file.relativePath.indexOf(filename) && file.domain === domain
-        }
-        return false
-      })
-      if (fileitem) {
-        let filepath = fileitem.relativePath
-        let file = fileHash(filepath, domain)
-        return skypeAccountsReport(backup, file)
-      } else return [] // Return an empty array to the formatter, since no main.db file can be found in the manifest
-    })
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+  // Fields for apps report
+  output: {
           'Skype Name': el => el.skypename
-        }
-      })
-
-      resolve(result)
-    })
-    .catch((e) => {
-      console.log('[!] Encountered an Error:', e)
-    })
+  }
 }
 
-const skypeAccountsReport = (backup, file) => {
-  return new Promise((resolve, reject) => {
-    var database = backup.getDatabase(file)
-    try {
-      database.all(`
-      SELECT * 
-      FROM Accounts 
-      `,
-      (err, rows) => {
-        if (err) resolve(err)
-        resolve(rows)
-      })
-    } catch (e) {
-      reject(e)
-    }
-  })
+
+const skypeAccountsReport = (backup) => { 
+    return new Promise((resolve, reject) => {
+        backup.getManifest()
+            .then((items) => {
+                let filename = 'main.db'
+                let fileitem = items.find((file) => {
+                    if (file && file.relativePath) {
+                        return ~file.relativePath.indexOf(filename) && file.domain === domain
+                    }
+                    return false
+                })
+                
+                if (fileitem) {
+                    let filepath = fileitem.relativePath
+                    let file = fileHash(filepath, domain)
+                    
+                    backup.openDatabase(file).then(database => {
+                        database.all(`SELECT * FROM Accounts `, (err, rows) => {
+                            if (err) resolve(err)
+                            resolve(rows);
+                        })
+                    }).catch(reject)
+                } else reject("Cannot find main.db"); // Return an empty array to the formatter, since no main.db file can be found in the manifest
+            });
+    })
 }

+ 34 - 48
tools/reports/thirdparty/skype/calls.js

@@ -3,65 +3,51 @@ const fileHash = require('../../../util/backup_filehash')
 
 const domain = 'AppDomain-com.skype.skype'
 
-module.exports.name = 'skype_calls'
-module.exports.description = 'Show Skype calls'
 
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
+module.exports = {
+  version: 4,
+  name: 'skype_calls',
+  description: `Show Skype calls`,
+  requiresBackup: true,
 
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return skypeAccountsReport(backup)
+    },
 
-module.exports.func = function (program, backup, resolve, reject) {
-  backup.getFileManifest()
-    .then((items) => {
-      let filename = 'main.db'
-      let fileitem = items.find((file) => {
-        if (file && file.relativePath) {
-          return ~file.relativePath.indexOf(filename) && file.domain === domain
-        }
-        return false
-      })
-      if (fileitem) {
-        let filepath = fileitem.relativePath
-        let file = fileHash(filepath, domain)
-        return skypeAccountsReport(backup, file)
-      } else return [] // Return an empty array to the formatter, since no main.db file can be found in the manifest
-    })
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+  // Fields for apps report
+  output: {
           'Begin Timestamp': el => (new Date((el.begin_timestamp) * 1000).toDateString()) + ' ' + (new Date((el.begin_timestamp) * 1000).toTimeString()),
           'Host Identity': el => el.host_identity,
           'Duration': el => el.duration ? el.duration : 'N/A',
           'Is Incoming': el => el.is_incoming === 1 ? 'Yes' : 'No',
           'Caller': el => el.caller_mri_identity
-        }
-      })
-
-      resolve(result)
-    })
-    .catch((e) => {
-      console.log('[!] Encountered an Error:', e)
-    })
+  }
 }
 
+
 const skypeAccountsReport = (backup, file) => {
   return new Promise((resolve, reject) => {
-    var database = backup.getDatabase(file)
-    try {
-      database.all(`
-      SELECT * 
-      FROM Calls 
-      `,
-      (err, rows) => {
-        if (err) return reject(err)
-        resolve(rows)
-      })
-    } catch (e) {
-      reject(e)
-    }
+      backup.getManifest()
+          .then((items) => {
+              let filename = 'main.db'
+              let fileitem = items.find((file) => {
+                  if (file && file.filename) {
+                      return ~file.filename.indexOf(filename) && file.domain === domain
+                  }
+                  return false
+              })
+              if (fileitem) {
+                  let filepath = fileitem.filename
+                  let file = fileHash(filepath, domain)
+                  backup.openDatabase(file).then(database => {
+                  database.all(` SELECT * FROM Calls `,
+                               (err, rows) => {
+                                   if (err) return reject(err)
+                                   resolve(rows)
+                               })
+                  }).catch(reject)
+              } else reject("Cannot find main.db") // Return an empty array to the formatter, since no main.db file can be found in the manifest
+    })
   })
 }

+ 16 - 22
tools/reports/thirdparty/spotify/searches.js

@@ -6,31 +6,25 @@ const fileHash = require('../../../util/backup_filehash')
 
 const database = fileHash('Library/Preferences/com.spotify.client.plist', 'AppDomain-com.spotify.client')
 
-module.exports.name = 'spotify.searches'
-module.exports.description = 'List associated Spotify account and its usage information'
-
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
-
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  spotifyReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+
+module.exports = {
+  version: 4,
+  name: 'spotify.searches',
+  description: `List associated Spotify account and its usage information`,
+  requiresBackup: true,
+
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return spotifyReport(backup)
+    },
+
+  // Fields for apps report
+  output: {
           'Username': el => el.username,
           'Type': el => el.placeholderIconIdentifier ? el.placeholderIconIdentifier.toLowerCase() : 'song',
           'Title': el => el.title,
           'Subtitle': el => el.subtitle
-        }
-      })
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
 const spotifyReport = (backup) => {
@@ -40,7 +34,7 @@ const spotifyReport = (backup) => {
       let spotifyData = bplist.parseBuffer(fs.readFileSync(filename))[0]
       let spotifyResult = []
 
-      // console.log('spotifyData', spotifyData)
+      console.log('spotifyData', spotifyData)
       // Get spotify username
       if (Object.keys(spotifyData).some((key) => ~key.indexOf('.com.spotify'))) {
         const keys = Object.keys(spotifyData).filter((key) => ~key.indexOf('.com.spotify'))

+ 27 - 35
tools/reports/thirdparty/viber/calls.js

@@ -1,53 +1,45 @@
 // Derive filenames based on domain + file path
 const fileHash = require('../../../util/backup_filehash')
+const apple_timestamp = require('../../../util/apple_timestamp')
 
 const database = fileHash('com.viber/database/Contacts.data', 'AppDomainGroup-group.viber.share.container')
 
-module.exports.name = 'viber_calls'
-module.exports.description = 'List Viber calls'
+module.exports = {
+  version: 4,
+  name: 'viber_calls',
+  description: `List Viber calls`,
+  requiresBackup: true,
 
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return viberCallsReport(backup)
+    },
 
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  viberCallsReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+  // Fields for apps report
+  output: {
           'PK': el => el.Z_PK,
-          'Date': el => (new Date((el.ZDATE + 978307200) * 1000).toDateString()) + ' ' + (new Date((el.ZDATE + 978307200) * 1000).toTimeString()),
+          'Date': el => el.ZDATE_STRING,
           'Name': el => el.ZMAINNAME + ' ' + el.ZSUFFIXNAME,
           'Phone': el => el.ZPHONENUMBER,
           'Call Type': el => el.ZCALLTYPE
-        }
-      })
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
+
 const viberCallsReport = (backup) => {
-  return new Promise((resolve, reject) => {
-    var vibercallsdb = backup.getDatabase(database)
-    try {
-      const query = `
-        SELECT * FROM ZRECENTSLINE
+    return new Promise((resolve, reject) => {
+        backup.openDatabase(database).then(database => {
+            const query = `
+        SELECT *, ${apple_timestamp.parse('ZRECENTSLINE.ZDATE')} AS ZDATE_STRING FROM ZRECENTSLINE
         INNER JOIN ZABCONTACT
         ON ZABCONTACT.Z_PK = ZRECENTSLINE.ZCONTACT
         ORDER BY ZABCONTACT.Z_PK;
-        `
-      vibercallsdb.all(query, async function (err, rows) {
-        if (err) reject(err)
-
-        resolve(rows)
-      })
-    } catch (e) {
-      reject(e)
-    }
-  })
+        `            
+            database.all(query, (err, rows) => {
+                if (err) resolve(err)
+                resolve(rows);
+            })
+        }).catch(reject)
+        
+    })
 }

+ 20 - 30
tools/reports/thirdparty/viber/contacts.js

@@ -3,49 +3,39 @@ const fileHash = require('../../../util/backup_filehash')
 
 const database = fileHash('com.viber/database/Contacts.data', 'AppDomainGroup-group.viber.share.container')
 
-module.exports.name = 'viber_contacts'
-module.exports.description = 'List Viber contacts'
+module.exports = {
+  version: 4,
+  name: 'viber_contacts',
+  description: `List Viber contacts`,
+  requiresBackup: true,
 
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return viberContactsReport(backup)
+    },
 
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  viberContactsReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+  // Fields for apps report
+  output: {
           'PK': el => el.Z_PK,
           'Name': el => el.ZDISPLAYFULLNAME,
           'Phone': el => el.ZPHONE
-        }
-      })
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
+
 const viberContactsReport = (backup) => {
   return new Promise((resolve, reject) => {
-    var vibercontactsdb = backup.getDatabase(database)
-    try {
-      const query = `
+      backup.openDatabase(database).then(database => {
+            const query = `
         SELECT * FROM ZMEMBER
         INNER JOIN ZPHONENUMBER
         ON ZPHONENUMBER.ZMEMBER = ZMEMBER.Z_PK
         ORDER BY ZMEMBER.Z_PK;
         `
-      vibercontactsdb.all(query, async function (err, rows) {
-        if (err) reject(err)
-
-        resolve(rows)
-      })
-    } catch (e) {
-      reject(e)
-    }
+          database.all(query, async function (err, rows) {
+              if (err) reject(err)
+              resolve(rows)
+          })
+      }).catch(reject)
   })
 }

+ 21 - 29
tools/reports/thirdparty/viber/messages.js

@@ -1,55 +1,47 @@
 // Derive filenames based on domain + file path
 const fileHash = require('../../../util/backup_filehash')
+const apple_timestamp = require('../../../util/apple_timestamp')
 
 const database = fileHash('com.viber/database/Contacts.data', 'AppDomainGroup-group.viber.share.container')
 
-module.exports.name = 'viber_messages'
-module.exports.description = 'List Viber messages'
 
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
+module.exports = {
+  version: 4,
+  name: 'viber_messages',
+  description: `List Viber messages`,
+  requiresBackup: true,
 
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return viberMessagesReport(backup)
+    },
 
-module.exports.func = function (program, backup, resolve, reject) {
-  viberMessagesReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+  // Fields for apps report
+  output: {
           'PK': el => el.Z_PK,
-          'Date': el => (new Date((el.ZDATE + 978307200) * 1000).toDateString()) + ' ' + (new Date((el.ZDATE + 978307200) * 1000).toTimeString()),
+          'Date': el => ZDATE_STRING,
           'Name': el => el.ZDISPLAYFULLNAME,
           'Text': el => el.ZTEXT,
           'State': el => el.ZSTATE
-        }
-      })
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
 const viberMessagesReport = (backup) => {
-  return new Promise((resolve, reject) => {
-    var vibermessagesdb = backup.getDatabase(database)
-    try {
-      const query = `
-        SELECT * FROM ZVIBERMESSAGE
+    return new Promise((resolve, reject) => {
+        console.log(database)
+     backup.openDatabase(database).then(database => {
+            const query = `
+        SELECT *, ${apple_timestamp.parse('ZVIBERMESSAGE.ZDATE')} AS ZDATE_STRING FROM ZVIBERMESSAGE
         INNER JOIN ZCONVERSATION
         ON ZCONVERSATION.Z_PK = ZVIBERMESSAGE.ZCONVERSATION
         INNER JOIN ZMEMBER
         ON ZCONVERSATION.ZINTERLOCUTOR = ZMEMBER.Z_PK
         ORDER BY ZVIBERMESSAGE.Z_PK DESC;
         `
-      vibermessagesdb.all(query, async function (err, rows) {
+      database.all(query, async function (err, rows) {
         if (err) reject(err)
-
         resolve(rows)
       })
-    } catch (e) {
-      reject(e)
-    }
+     }).catch(reject)
   })
 }

+ 25 - 36
tools/reports/thirdparty/waze/favorites.js

@@ -10,22 +10,19 @@ const fileHash = require('../../../util/backup_filehash')
 
 const database = fileHash('Documents/user.db', 'AppDomain-com.waze.iphone')
 
-module.exports.name = 'waze_favorites'
-module.exports.description = 'List Waze app favorite places'
-
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
-
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  wazeReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+module.exports = {
+  version: 4,
+  name: 'waze_favorites',
+  description: `List Waze app favorite places`,
+  requiresBackup: true,
+
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return wazeReport(backup)
+    },
+
+  // Fields for apps report
+  output: {
           'Name': el => el.name,
           'Modified Date': el => (new Date((el.modified_time) * 1000).toDateString()) + ' ' + (new Date((el.modified_time) * 1000).toTimeString()) ,
           'Latitude': el => el.latitude / 1000000,
@@ -34,11 +31,7 @@ module.exports.func = function (program, backup, resolve, reject) {
           'City': el => el.city,
           'State': el => el.state,
           'Country': el => el.country
-        }
-      })
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
 function KeyValue (property, plist) {
@@ -47,21 +40,17 @@ function KeyValue (property, plist) {
 }
 
 const wazeReport = (backup) => {
-  return new Promise((resolve, reject) => {
-    var wazedb = backup.getDatabase(database)
-      try {
+    return new Promise((resolve, reject) => {
         const query = `
         select FAVORITES.name, FAVORITES.created_time, FAVORITES.modified_time, FAVORITES.rank, PLACES.latitude, PLACES.longitude, PLACES.street, PLACES.city, PLACES.state, PLACES.country from FAVORITES
         left join PLACES on FAVORITES.place_id = PLACES.id
-        order by rank
-        `
-        wazedb.all(query, async function (err, rows) {
-          if (err) reject(err)
-
-          resolve(rows)
-        })
-      } catch (e) {
-        reject(e)
-      }
-  })
-}
+        order by rank  `
+        
+        backup.openDatabase(database).then(database => {
+            database.all(query, (err, rows) => {
+                if (err) resolve(err)
+                resolve(rows);
+            })
+        }).catch(reject)
+    })
+}

+ 20 - 25
tools/reports/thirdparty/waze/places.js

@@ -5,27 +5,26 @@ const bplist = require('bplist-parser')
 const fs = require('fs')
 const plist = require('plist')
 
+
 // Derive filenames based on domain + file path
 const fileHash = require('../../../util/backup_filehash')
 
 const database = fileHash('Documents/user.db', 'AppDomain-com.waze.iphone')
 
-module.exports.name = 'waze_places'
-module.exports.description = 'List Waze app places'
 
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
+module.exports = {
+  version: 4,
+  name: 'waze_places',
+  description: `List Waze app places`,
+  requiresBackup: true,
 
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return wazeReport(backup)
+    },
 
-module.exports.func = function (program, backup, resolve, reject) {
-  wazeReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+  // Fields for apps report
+  output: {
           'Name': el => el.name,
           'Created Date': el => (new Date((el.created_time) * 1000).toDateString()) + ' ' + (new Date((el.created_time) * 1000).toTimeString()) ,
           'Latitude': el => el.latitude / 1000000,
@@ -34,13 +33,11 @@ module.exports.func = function (program, backup, resolve, reject) {
           'City': el => el.city,
           'State': el => el.state,
           'Country': el => el.country
-        }
-      })
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
+
+
 function KeyValue (property, plist) {
   this.key = property
   this.value = plist[property] ? plist[property] : 'N/A'
@@ -48,19 +45,17 @@ function KeyValue (property, plist) {
 
 const wazeReport = (backup) => {
   return new Promise((resolve, reject) => {
-    var wazedb = backup.getDatabase(database)
-      try {
+    backup.openDatabase(database)
+    .then(db => {
         const query = `
         select * from PLACES
         order by id
         `
-        wazedb.all(query, async function (err, rows) {
+        db.all(query, async function (err, rows) {
           if (err) reject(err)
 
           resolve(rows)
         })
-      } catch (e) {
-        reject(e)
-      }
+    }).catch(reject)
   })
-}
+}

+ 24 - 33
tools/reports/thirdparty/waze/recents.js

@@ -10,22 +10,19 @@ const fileHash = require('../../../util/backup_filehash')
 
 const database = fileHash('Documents/user.db', 'AppDomain-com.waze.iphone')
 
-module.exports.name = 'waze_recents'
-module.exports.description = 'List Waze app recent destinations'
-
-// Specify this reporter requires a backup.
-// The second parameter to func() is now a backup instead of the path to one.
-module.exports.requiresBackup = true
-
-// Specify this reporter supports the promises API for allowing chaining of reports.
-module.exports.usesPromises = true
-
-module.exports.func = function (program, backup, resolve, reject) {
-  wazeReport(backup)
-    .then((items) => {
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
+module.exports = {
+  version: 4,
+  name: 'waze_recents',
+  description: `List Waze app recent destinations`,
+  requiresBackup: true,
+
+  // Run on a v3 lib / backup object.
+    run (lib, { backup }) {
+        return wazeReport(backup)
+    },
+
+  // Fields for apps report
+  output: {
           'Id': el => el.id,
           'Name': el => el.name,
           'Created Date': el => (new Date((el.created_time) * 1000).toDateString()) + ' ' + (new Date((el.created_time) * 1000).toTimeString()) ,
@@ -36,13 +33,10 @@ module.exports.func = function (program, backup, resolve, reject) {
           'City': el => el.city,
           'State': el => el.state,
           'Country': el => el.country
-        }
-      })
-      resolve(result)
-    })
-    .catch(reject)
+  }
 }
 
+
 function KeyValue (property, plist) {
   this.key = property
   this.value = plist[property] ? plist[property] : 'N/A'
@@ -50,20 +44,17 @@ function KeyValue (property, plist) {
 
 const wazeReport = (backup) => {
   return new Promise((resolve, reject) => {
-    var wazedb = backup.getDatabase(database)
-      try {
-        const query = `
+      backup.openDatabase(database).then(database => {
+          const query = `
         select RECENTS.name, RECENTS.created_time, RECENTS.access_time, RECENTS.id, PLACES.latitude, PLACES.longitude, PLACES.street, PLACES.city, PLACES.state, PLACES.country from RECENTS
         left join PLACES on RECENTS.place_id = PLACES.id
         order by RECENTS.id
         `
-        wazedb.all(query, async function (err, rows) {
-          if (err) reject(err)
-
-          resolve(rows)
-        })
-      } catch (e) {
-        reject(e)
-      }
+          database.all(query, async function (err, rows) {
+              if (err) reject(err)
+              
+              resolve(rows)
+          })
+      }).catch(reject)
   })
-}
+}

+ 8 - 0
tools/util/apple_timestamp.js

@@ -0,0 +1,8 @@
+
+module.exports = {
+  parse: (field_name) => {
+    return `CASE WHEN (${field_name} > 1000000000) THEN datetime(${field_name} / 1000000000 + 978307200, 'unixepoch') 
+                 WHEN ${field_name} <> 0 THEN datetime((${field_name} + 978307200), 'unixepoch') 
+                 ELSE ${field_name} END`
+  }
+}