Skip to content
Merged
72 changes: 49 additions & 23 deletions scripts/artifacts/LinkedIn.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
# LinkedIn App (com.linkedin.android)
# Author: Marco Neumann (kalinko@be-binary.de)
# Version: 0.0.1
#
# Tested with the following versions:
# 2024-08-16: Android 14, App: 4.1966
# 2024-08-16: Android 14, App: 4.1.966
# 2025-02-07: Android 16, App: 4.1.1166

# Requirements: json, xml


__artifacts_v2__ = {


"get_linkedin_account": {
"linkedin_account": {
"name": "LinkedIn - Account",
"description": "Existing account in LinkedIn App. The Public Identifier can be used to visit the public profile on the LinkedIn Website (https://www.linkedin.com/in/[Public Identifier]).",
"author": "Marco Neumann {kalinko@be-binary.de}",
"version": "0.0.1",
"date": "2025-04-26",
"requirements": "xml",
"version": "0.1",
"creation_date": "2025-04-26",
'last_update_date': '2026-02-07',
"requirements": "xml, json",
"category": "LinkedIn",
"notes": "",
"paths": ('*/com.linkedin.android/shared_prefs/linkedInPrefsName.xml'),
"output_types": "standard",
"artifact_icon": "user"
},
"get_linkedin_messages": {
"linkedin_messages": {
"name": "LinkedIn - Messages",
"description": "Messages sent and received from LinkedIn App",
"author": "Marco Neumann {kalinko@be-binary.de}",
"version": "0.0.1",
"date": "2025-04-26",
"version": "0.1",
'creation_date': '2025-04-26',
'last_update_date': '2026-02-07',
"requirements": "",
"category": "LinkedIn",
"notes": "",
Expand All @@ -39,14 +38,13 @@
}
}


import json
import xml.etree.ElementTree as ET

from scripts.ilapfuncs import artifact_processor, convert_unix_ts_to_utc, get_sqlite_db_records

@artifact_processor
def get_linkedin_account(files_found, report_folder, seeker, wrap_text):
def linkedin_account(files_found, _report_folder, _seeker, _wrap_text):

# Get data from xml into a dict to work with
xml_dict = {}
Expand Down Expand Up @@ -74,21 +72,29 @@ def get_linkedin_account(files_found, report_folder, seeker, wrap_text):
first_name = temp_meModel['miniProfile']['firstName']
headline = temp_meModel['miniProfile']['occupation']
public_identifier = temp_meModel['miniProfile']['publicIdentifier']
data_list = [(last_login, member_id, last_name, first_name, headline, public_identifier)]

data_headers = ('Last Login', 'Member ID', 'Last Name', 'First Name', 'Headline', 'Public Identifier')
data_list = [(last_login, member_id, account_mail, last_name, first_name, headline, public_identifier)]

data_headers = (
('Last Login', 'datetime'),
'Member ID',
'Account Mail',
'Last Name',
'First Name',
'Headline',
'Public Identifier'
)

return data_headers, data_list, files_found[0]



@artifact_processor
def get_linkedin_messages(files_found, report_folder, seeker, wrap_text):
def linkedin_messages(files_found, _report_folder, _seeker, _wrap_text):
files_found = [x for x in files_found if not x.endswith('wal') and not x.endswith('shm')]

query = ('''
SELECT
strftime('%Y-%m-%d %H:%M:%S.', "md"."deliveredAt"/1000, 'unixepoch') || ("md"."deliveredAt"%1000) [deliveredAt],
md.deliveredAt[deliveredAt],
CASE WHEN md.status = '5'
THEN 'Delivered'
ELSE 'Unknown'
Expand All @@ -109,7 +115,7 @@ def get_linkedin_messages(files_found, report_folder, seeker, wrap_text):

data_list = []
for row in db_records:
delivery_date = row[0]
delivery_date = convert_unix_ts_to_utc(int(row[0])/1000)
delivery_status = row[1]
sender_firstname = row[2]
sender_lastname = row[3]
Expand All @@ -120,8 +126,28 @@ def get_linkedin_messages(files_found, report_folder, seeker, wrap_text):
message = row[8]
conversationurn = row[9]

data_list.append((delivery_date, delivery_status, sender_firstname, sender_lastname, sender_headline, sender_profile_url, sender_distance, subject, message, conversationurn))

data_headers = ('Delivery Date', 'Delivery Status', 'Sender First Name', 'Sender Last Name', 'Sender Headline', 'Sender Profile Url', 'Sender Distance', 'Subject', 'Message', 'Conversation Urn')
data_list.append( (delivery_date,
delivery_status,
sender_firstname,
sender_lastname,
sender_headline,
sender_profile_url,
sender_distance,
subject,
message,
conversationurn)
)

data_headers = ( ('Delivery Date', 'datetime'),
'Delivery Status',
'Sender First Name',
'Sender Last Name',
'Sender Headline',
'Sender Profile Url',
'Sender Distance',
'Subject',
'Message',
'Conversation Urn'
)

return data_headers, data_list, files_found[0]
245 changes: 245 additions & 0 deletions scripts/artifacts/RandoChat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# Android RandoChat App (com.random.chat.app)

# Tested Version: 6.3.3


__artifacts_v2__ = {
'randochat_messages': {
'name': 'RandoChat Messages',
'description': 'Parses RandoChat App Messages',
'author': 'Marco Neumann {kalinko@be-binary.de}',
'version': '0.0.1',
'creation_date': '2026-01-15',
'last_update_date': '2026-01-15',
'requirements': 'os, path',
'category': 'Chats',
'notes': '',
'paths': (
'*/data/com.random.chat.app/databases/ramdochatV2.db*',
'*/Android/data/com.random.chat.app/files/Pictures/RandoChat/*',
'*/Android/data/com.random.chat.app/files/images/*',
'*/Android/data/com.random.chat.app/files/Music/RandoChat/*'
),
'output_types': 'standard',
'artifact_icon': 'message-square',
"html_columns": ["Media File"]
},
'randochat_account': {
'name': 'RandoChat Accounts',
'description': 'Parses RandoChat App Accounts',
'author': 'Marco Neumann {kalinko@be-binary.de}',
'version': '0.0.1',
'creation_date': '2026-01-15',
'last_update_date': '2026-01-15',
'requirements': '',
'category': 'Accounts',
'notes': '',
'paths': (
'*/data/com.random.chat.app/databases/ramdochatV2.db*'
),
'output_types': 'standard',
'artifact_icon': 'user'
},
'randochat_contacts': {
'name': 'RandoChat Contacts',
'description': 'Parses RandoChat App Contacts',
'author': 'Marco Neumann {kalinko@be-binary.de}',
'version': '0.0.1',
'creation_date': '2026-01-15',
'last_update_date': '2026-01-15',
'requirements': '',
'category': 'Contacts',
'notes': '',
'paths': (
'*/data/com.random.chat.app/databases/ramdochatV2.db*'
),
'output_types': 'standard',
'artifact_icon': 'user'
}
}

import os

from scripts.ilapfuncs import artifact_processor, convert_unix_ts_to_utc, get_sqlite_db_records, media_to_html

@artifact_processor
def randochat_messages(files_found, report_folder, _seeker, _wrap_text):
files_found = [x for x in files_found if not x.endswith('wal') and not x.endswith('shm')]


# Get the different files found and store their pathes in corresponding lists to work with them
main_db = ''
attachments = []

for file_found in files_found:
file_found = str(file_found)

if file_found.endswith('ramdochatV2.db'):
main_db = file_found

if 'files' in os.path.dirname(file_found):
attachments.append(file_found)


query = '''
SELECT
m.hora [Timestamp],
m.mensagem [Message Content],
c.apelido [Contact Username],
m.minha [Sent?], -- 1 = sent, 2 = received
m.url [Media File],
m.id_talk_server [Conversation ID],
m.id_servidor [Message ID]
FROM mensagens m
LEFT JOIN conversa c ON c.id_server = m.id_talk_server
'''

db_records = get_sqlite_db_records(main_db, query)
data_list = []

for row in db_records:
timestamp = convert_unix_ts_to_utc(int(row[0])/1000)
content = row[1]
contact_name = row[2]
direction = row[3]
media_file = row[4]
conv_id = row[5]
message_id = row[6]

# Handling attachments
if media_file is None:
attachment = 0
else:
attachment = ''
for att_path in attachments:
if os.path.basename(media_file) in os.path.basename(att_path):
attachment = media_to_html(os.path.basename(att_path), attachments, report_folder)

data_list.append((timestamp, content, contact_name, direction, attachment, conv_id, message_id))

data_headers = (
('Timestamp', 'datetime'),
'Content',
'Contact Username',
'Sent?',
'Media File',
'Conversation ID',
'Message ID'
)

return data_headers, data_list, main_db


@artifact_processor
def randochat_account(files_found, _report_folder, _seeker, _wrap_text):
files_found = [x for x in files_found if not x.endswith('wal') and not x.endswith('shm')]


# Get the different files found and store their pathes in corresponding lists to work with them
main_db = ''

for file_found in files_found:
file_found = str(file_found)

if file_found.endswith('ramdochatV2.db'):
main_db = file_found


query = '''
SELECT
MAX(CASE WHEN name LIKE 'apelido' THEN value END) [Username],
MAX(CASE WHEN name LIKE 'sexo' THEN (CASE WHEN value = 'H' THEN 'Male' WHEN value = 'M' THEN 'Female' END) END) [User Sex],
MAX(CASE WHEN name LIKE 'idade' THEN value END) [User Age],
MAX(CASE WHEN name LIKE 'language' THEN value END) [Language],
MAX(CASE WHEN name LIKE 'device_id' THEN value END) [Device ID],
MAX(CASE WHEN name LIKE 'idade_de' THEN value END) [Preferred Age From],
MAX(CASE WHEN name LIKE 'idade_ate' THEN value END) [Preferred Age To],
MAX(CASE WHEN name LIKE 'sexo_search' THEN (CASE WHEN value = 'H' THEN 'Male' WHEN value = 'M' THEN 'Female' END) END) [Preferred Sex]
FROM configuracao
'''

db_records = get_sqlite_db_records(main_db, query)
data_list = []

for row in db_records:
username = row[0]
sex = row[1]
age = row[2]
language = row[3]
device_id = row[4]
age_from = row[5]
age_to = row[6]
preferred_sex = row[7]


data_list.append((username, sex, age, language, device_id, age_from, age_to, preferred_sex))

data_headers = (
'Username',
'User Sex',
'User Age',
'Language',
'Device ID',
'Preferred Age From',
'Preferred Age To',
'Preferred Sex'
)

return data_headers, data_list, main_db


@artifact_processor
def randochat_contacts(files_found, _report_folder, _seeker, _wrap_text):
files_found = [x for x in files_found if not x.endswith('wal') and not x.endswith('shm')]
main_db = ''

for file_found in files_found:
file_found = str(file_found)

if file_found.endswith('ramdochatV2.db'):
main_db = file_found


query = '''
SELECT
c.id_pessoa [Account ID],
c.apelido [Username],
c.idade [Age],
CASE
WHEN c.sexo = 'M' THEN 'Female' -- From Mulher
WHEN c.sexo = 'H' THEN 'Male' -- From Homem
END [Sex],
CASE
WHEN c.favorite = 1 THEN 'Yes'
WHEN c.favorite = 0 THEN 'No'
END [Favorite?],
CASE
WHEN c.bloqueado = 1 THEN 'Yes'
WHEN c.bloqueado = 0 THEN 'No'
END [Blocked?],
CASE WHEN
c.images = '' THEN 'n/a'
ELSE
json_extract(c.images, "$[0].img")
END [Link Profile Pic]
FROM conversa c
'''

db_records = get_sqlite_db_records(main_db, query)
data_list = []

for row in db_records:
account_id = row[0]
username = row[1]
age = row[2]
sex = row[3]
favorite = row[4]
blocked = row[5]
profile_pic = row[6]


data_list.append((account_id, username, age, sex, favorite, blocked, profile_pic))

data_headers = ('Account ID', 'Username', 'Age', 'Sex', 'Favorite?', 'Blocked?', 'Link Profile Pic')

return data_headers, data_list, main_db
Loading