$line) { if ($k === 0) { continue; } $fields = explode(',', $line); if (count($fields) === 4 && $fields[1] === 'admin@admin.test') { $this->__servers[] = [ 'Server' => [ 'url' => trim($fields[0]), 'authkey' => trim($fields[2]) ] ]; } } } public function getOptionParser() { $parser = parent::getOptionParser(); $parser->addSubcommand('enableTaxonomy', [ 'help' => __('Enable a taxonomy with all its tags.'), 'parser' => array( 'options' => array( 'instances' => [ 'help' => 'Path to the instance file, by default "instances.csv" from the local directory', 'short' => 'i', 'required' => true ], 'taxonomy' => [ 'help' => 'The name of the taxonomy to enable, such as "tlp"', 'short' => 't', 'required' => true ], 'misp_url_filter' => [ 'help' => 'The url of the instance to enable it for - otherwise all are selected', 'short' => 'm', 'required' => false ] ), ), ]); $parser->addSubcommand('checkSyncConnections', [ 'help' => __('Check the given sync connection(s) for the given server(s).'), 'parser' => array( 'options' => array( 'instances' => [ 'help' => 'Path to the instance file, by default "instances.csv" from the local directory', 'short' => 'i', 'required' => true ], 'misp_url_filter' => [ 'help' => 'The url of the instance to execute changes on. If not set, all are updated.', 'short' => 'm', 'required' => false ], 'synced_misp_url_filter' => [ 'help' => 'The sync connection to modify on each valid instance (as selected by the misp_url_filter). If not set, all sync connections on the selected instances will be updated.', 'short' => 's', 'required' => false ] ), ), ]); $parser->addSubcommand('modifySyncConnection', [ 'help' => __('Modify sync connection(s).'), 'parser' => array( 'options' => array( 'instances' => [ 'help' => 'Path to the instance file, by default "instances.csv" from the local directory', 'short' => 'i', 'required' => true ], 'misp_url_filter' => [ 'help' => 'The url of the instance to execute changes on. If not set, all are updated.', 'short' => 'm', 'required' => false ], 'synced_misp_url_filter' => [ 'help' => 'The sync connection to modify on each valid instance (as selected by the misp_url_filter). If not set, all sync connections on the selected instances will be updated.', 'short' => 's', 'required' => false ], 'json' => [ 'help' => 'JSON delta to push (such as \'{"push": 1}\').', 'short' => 'j', 'required' => true ] ), ), ]); $parser->addSubcommand('addWarninglist', [ 'help' => __('Inject warninglist'), 'parser' => array( 'options' => array( 'instances' => [ 'help' => 'Path to the instance file, by default "instances.csv" from the local directory', 'short' => 'i', 'required' => true ], 'warninglist' => [ 'help' => 'Path to the warninglist file', 'short' => 'w', 'required' => true ] ), ), ]); $parser->addSubcommand('status', [ 'help' => __('Check if the instances are available / the API key works.'), 'parser' => array( 'options' => array( 'instances' => [ 'help' => 'Path to the instance file, by default "instances.csv" from the local directory', 'short' => 'i', 'required' => true ] ), ), ]); $parser->addSubcommand('scores', [ 'help' => __('Generate the scores for all BTs.'), 'parser' => array( 'options' => array( 'instances' => [ 'help' => 'Path to the instance file, by default "instances.csv" from the local directory', 'short' => 'i', 'required' => true ], 'server_url' => [ 'help' => 'URL of the server to query for the scores. If nothing is specified, the first valid entry from instances.csv is taken.', 'short' => 's', 'required' => false ], 'from' => [ 'help' => 'Lower bound of the date. Accepts timestamp or date distance (such as 1d or 5h). Defaults to unbounded.', 'short' => 'f', 'required' => false ], 'to' => [ 'help' => 'Upper bound of the date. Accepts timestamp or date distance (such as 1d or 5h). Defaults to unbounded.', 'short' => 't', 'required' => false ], 'org' => [ 'help' => 'Name the org that should be evaluated. If not set, all will be included.', 'short' => 'o', 'required' => false ] ), ), ]); return $parser; } public function checkSyncConnections() { $this->__getInstances($this->param('instances')); $results = []; $instanceFilter = $this->param('misp_url_filter'); $syncedInstanceFilter = $this->param('synced_misp_url_filter'); foreach ($this->__servers as $server) { if (!empty($instanceFilter) && strtolower(trim($server['Server']['url'])) !== strtolower(trim($instanceFilter))) { continue; } $HttpSocket = $this->Server->setupHttpSocket($server, null); $request = $this->Server->setupSyncRequest($server, 'Server'); $start_time = microtime(true); $response = $HttpSocket->get($server['Server']['url'] . '/servers/index', false, $request); $baseline = round((microtime(true) - $start_time) * 1000); if (!$response->isOk()) { $this->out($server['Server']['url'] . ': ' . 'Connection or auth failed', 1, Shell::NORMAL); continue; } $synced_servers = json_decode($response->body, true); foreach ($synced_servers as $synced_server) { $success = false; if (empty($syncedInstanceFilter) || strtolower($synced_server['Server']['url']) === strtolower($syncedInstanceFilter)) { $start_time = microtime(true); $response = $HttpSocket->get($server['Server']['url'] . '/servers/testConnection/' . $synced_server['Server']['id'], '{}', $request); $execution_time = round((microtime(true) - $start_time) * 1000) - $baseline; if ($response->isOk()) { $success = true; } $this->out( sprintf( '%s connection to %s: %s (%sms)', $server['Server']['url'], $synced_server['Server']['url'], sprintf( '<%s>%s', $success ? 'info' : 'error', $success ? 'Success' : 'Failed', $success ? 'info' : 'error' ), $execution_time ), 1, Shell::NORMAL ); } } } } public function modifySyncConnection() { $this->__getInstances($this->param('instances')); $results = []; $instanceFilter = $this->param('misp_url_filter'); $syncedInstanceFilter = $this->param('synced_misp_url_filter'); $json = $this->param('json'); foreach ($this->__servers as $server) { if (!empty($instanceFilter) && strtolower(trim($server['Server']['url'])) !== strtolower(trim($instanceFilter))) { continue; } $HttpSocket = $this->Server->setupHttpSocket($server, null); $request = $this->Server->setupSyncRequest($server, 'Server'); $response = $HttpSocket->get($server['Server']['url'] . '/servers/index', false, $request); if (!$response->isOk()) { $this->out($server['Server']['url'] . ': ' . 'Connection or auth failed', 1, Shell::NORMAL); } $synced_servers = json_decode($response->body, true); $success = false; foreach ($synced_servers as $synced_server) { if (empty($syncedInstanceFilter) || strtolower($synced_server['Server']['url']) === strtolower($syncedInstanceFilter)) { debug($json); $response = $HttpSocket->post($server['Server']['url'] . '/servers/edit/' . $synced_server['Server']['id'], $json, $request); debug($response->body); if ($response->isOk()) { $success = true; } $this->out( sprintf( '%s connection to %s: %s', $server['Server']['url'], $synced_server['Server']['url'], sprintf( '<%s>%s', $success ? 'info' : 'error', $success ? 'Success' : 'Failed', $success ? 'info' : 'error' ) ), 1, Shell::NORMAL ); } } } } public function enableTaxonomy() { $taxonomyToEnable = $this->param('taxonomy'); $instanceFilter = $this->param('misp_url_filter'); if (empty($taxonomyToEnable)) { $this->error('No taxonomy provided', 'Provide a taxonomy by specifying the -t or --taxonomy options.'); } $this->__getInstances($this->param('instances')); $results = []; foreach ($this->__servers as $server) { if (!empty($instanceFilter) && strtolower(trim($server['Server']['url'])) !== strtolower(trim($instanceFilter))) { continue; } $HttpSocket = $this->Server->setupHttpSocket($server, null); $request = $this->Server->setupSyncRequest($server, 'Server'); $response = $HttpSocket->get($server['Server']['url'] . '/taxonomies/index', false, $request); if (!$response->isOk()) { $this->out($server['Server']['url'] . ': ' . 'Connection or auth failed', 1, Shell::NORMAL); } $taxonomies = json_decode($response->body, true); $success = false; foreach ($taxonomies as $taxonomy) { if ($taxonomy['Taxonomy']['namespace'] === $taxonomyToEnable) { $response = $HttpSocket->post($server['Server']['url'] . '/taxonomies/enable/' . $taxonomy['Taxonomy']['id'], '{}', $request); if ($response->isOk()) { $response = $HttpSocket->post($server['Server']['url'] . '/taxonomies/addTag/' . $taxonomy['Taxonomy']['id'], '{}', $request); if ($response->isOk()) { $success = true; } } } } $results[$server['Server']['url']] = $success ? 'Success' : 'Failed'; $statusWrapped = sprintf( '<%s>%s', $success ? 'info' : 'error', $results[$server['Server']['url']], $success ? 'info' : 'error' ); $this->out($server['Server']['url'] . ': ' . $statusWrapped, 1, Shell::NORMAL); } } public function status() { $this->__getInstances($this->param('instances')); $results = []; foreach ($this->__servers as $server) { $HttpSocket = $this->Server->setupHttpSocket($server, null); $request = $this->Server->setupSyncRequest($server, 'Server'); $start_time = microtime(true); $fatal_error = false; try { $response = $HttpSocket->get($server['Server']['url'] . '/users/view/me', false, $request); } catch (Exception $e) { $fatal_error = true; echo "\x07"; $statusWrapped = sprintf( '%s %s: %s', 'Something went wrong while trying to reach', $server['Server']['url'], $e->getMessage() ); } if (!$fatal_error) { $execution_time = round((microtime(true) - $start_time) * 1000); $statusWrapped = sprintf( '<%s>%s', $response->isOk() ? 'info' : 'error', $response->isOk() ? 'OK (' . $execution_time . 'ms)' : 'Failed. (' . $response->code . ')', $response->isOk() ? 'info' : 'error' ); } $this->out($server['Server']['url'] . ': ' . $statusWrapped, 1, Shell::NORMAL); } } public function addWarninglist() { $path = $this->param('warninglist'); if (empty($path)) { $this->error('No warninglist provided', 'Provide a path to a file containing a warninglist JSON by specifying the -w or --warninglist options.'); } $file = file_get_contents($path); $this->__getInstances($this->param('instances')); $results = []; foreach ($this->__servers as $server) { $HttpSocket = $this->Server->setupHttpSocket($server, null); $request = $this->Server->setupSyncRequest($server, 'Server'); $start_time = microtime(true); $response = $HttpSocket->post($server['Server']['url'] . '/warninglists/add', $file, $request); $statusWrapped = sprintf( '<%s>%s', $response->isOk() ? 'info' : 'error', $response->isOk() ? 'OK' : 'Could not create warninglist', $response->isOk() ? 'info' : 'error' ); $this->out($server['Server']['url'] . ': ' . $statusWrapped, 1, Shell::NORMAL); } } public function scores() { $results = []; $this->__getInstances($this->param('instances')); $server = null; if (!empty($this->param['server_url'])) { foreach ($this->__servers as $temp_server) { if ($temp_server['Server']['url'] === $this->param['server_url']) { $server = $temp_server; } } } else { $server = $this->__servers[0]; } $HttpSocket = $this->Server->setupHttpSocket($server, null); $request = $this->Server->setupSyncRequest($server); $response = $HttpSocket->get($server['Server']['url'] . '/organisations/index/scope:all', false, $request); $orgs = json_decode($response->body(), true); $this->out(__('Organisations fetched. %d found.', count($orgs)), 1, Shell::VERBOSE); $org_mapping = []; foreach ($orgs as $org) { if (!empty($this->param('org')) && $org['Organisation']['name'] !== $this->param('org')) { continue; } if ($org['Organisation']['name'] === 'YT') { continue; } $org_mapping[$org['Organisation']['name']] = $org['Organisation']['id']; } if (!empty($this->param['from'])) { $time_range[] = $this->param['from']; } if (!empty($this->param['to'])) { if (empty($time_range)) { $time_range[] = '365d'; } $time_range[] = $this->param['to']; } foreach ($org_mapping as $org_name => $org_id) { $time_range = []; $params = [ 'org' => $org_id ]; if (!empty($time_range)) { $params['publish_timestamp'] = $time_range; } $response = $HttpSocket->post($server['Server']['url'] . '/events/restSearch', json_encode($params), $request); $events = json_decode($response->body(), true); $this->out(__('Events fetched from %s. %d found.', $org_name, count($events['response'])), 1, Shell::VERBOSE); $results[$org_name] = [ 'attribute_count' => 0, 'object_count' => 0, 'connected_elements' => 0, 'event_tags' => 0, 'attribute_tags' => 0, 'attack' => 0, 'other' => 0, 'attribute_attack' => 0, 'attribute_other' => 0, 'score' => 0, 'warnings' => 0 ]; foreach ($events['response'] as $event) { if (!empty($event['Event']['Tag'])) { foreach ($event['Event']['Tag'] as $tag) { if (substr($tag['name'], 0, 32) === 'misp-galaxy:mitre-attack-pattern') { $results[$org_name]['attack'] += 1; } else { $results[$org_name]['other'] += 1; } } } if (!empty($event['Event']['Galaxy'])) { foreach ($event['Event']['Galaxy'] as $galaxy) { if ($galaxy['type'] === 'mitre-attack-pattern') { $results[$org_name]['attack'] += 1; } else { $results[$org_name]['other'] += 1; } } } foreach ($event['Event']['Attribute'] as $attribute) { if (!empty($attribute['referenced_by'])) { $results[$org_name]['connected_elements'] +=1; } if (!empty($attribute['Tag'])) { foreach ($attribute['Tag'] as $tag) { if (substr($tag['name'], 0, 32) === 'misp-galaxy:mitre-attack-pattern') { $results[$org_name]['attribute_attack'] += 1; } else { $results[$org_name]['attribute_other'] += 1; } } } if (!empty($attribute['warnings'])) { $result[$org_name]['warnings'] += 1; } } $results[$org_name]['attribute_count'] += count($event['Event']['Attribute']); if (!empty($event['Event']['Object'])) { foreach ($event['Event']['Object'] as $object) { $results[$org_name]['attribute_count'] += count($object['Attribute']); $results[$org_name]['object_count'] += 1; if (!empty($object['ObjectReference'])) { $results[$org_name]['connected_elements'] += 1; } foreach ($object['Attribute'] as $attribute) { if (!empty($attribute['Tag'])) { foreach ($attribute['Tag'] as $tag) { if (substr($tag['name'], 0, 32) === 'misp-galaxy:mitre-attack-pattern') { $results[$org_name]['attribute_attack'] += 1; } else { $results[$org_name]['attribute_other'] += 1; } } } } } } } } $scores = []; foreach ($results as $k => $result) { $totalCount = $result['attribute_count'] + $result['object_count']; if ($totalCount) { if (empty($result['warnings'])) { $results[$k]['metrics']['warnings'] = 100; } else if (100 * $result['warnings'] < $result['attribute_count']) { $results[$k]['metrics']['warnings'] = 50; } else { $results[$k]['metrics']['warnings'] = 0; } $results[$k]['metrics']['connectedness'] = 100 * ($result['connected_elements'] / ($result['attribute_count'] + $result['object_count'])); $results[$k]['metrics']['attack_weight'] = 100 * (2*($result['attack']) + $result['attribute_attack']) / ($result['attribute_count'] + $result['object_count']); $results[$k]['metrics']['other_weight'] = 100 * (2*($result['other']) + $result['attribute_other']) / ($result['attribute_count'] + $result['object_count']); } foreach (['connectedness', 'attack_weight', 'other_weight', 'warnings'] as $metric) { if (empty($results[$k]['metrics'][$metric])) { $results[$k]['metrics'][$metric] = 0; } if ($results[$k]['metrics'][$metric] > 100) { $results[$k]['metrics'][$metric] = 100; } } $results[$k]['score'] = round( 20 * $results[$k]['metrics']['warnings'] + 20 * $results[$k]['metrics']['connectedness'] + 40 * $results[$k]['metrics']['attack_weight'] + 20 * $results[$k]['metrics']['other_weight'] ) / 100; $scores[$k]['total'] = $results[$k]['score']; $scores[$k]['warnings'] = round(20 * $results[$k]['metrics']['warnings']); $scores[$k]['connectedness'] = round(20 * $results[$k]['metrics']['connectedness']); $scores[$k]['attack_weight'] = round(40 * $results[$k]['metrics']['attack_weight']); $scores[$k]['other_weight'] = round(20 * $results[$k]['metrics']['other_weight']); } arsort($scores, SORT_DESC); $this->out(str_repeat('=', 128), 1, Shell::NORMAL); $this->out(sprintf( '| %s | %s | %s |', str_pad('Org', 10, ' ', STR_PAD_RIGHT), str_pad('Graph', 100, ' ', STR_PAD_RIGHT), str_pad('Score', 8, ' ', STR_PAD_RIGHT) ), 1, Shell::NORMAL); $this->out(str_repeat('=', 128), 1, Shell::NORMAL); foreach ($scores as $org => $score) { $score_string[0] = str_repeat('█', round($score['warnings']/100)); $score_string[1] = str_repeat('█', round($score['connectedness']/100)); $score_string[2] = str_repeat('█', round($score['attack_weight']/100)); $score_string[3] = str_repeat('█', round($score['other_weight']/100)); $this->out(sprintf( '| %s | %s | %s |', str_pad($org, 10, ' ', STR_PAD_RIGHT), sprintf( '%s%s%s%s%s', $score_string[0], $score_string[1], $score_string[2], $score_string[3], str_repeat(' ', 100 - mb_strlen(implode('', $score_string))) ), str_pad($score['total'] . '%', 8, ' ', STR_PAD_RIGHT) ), 1, Shell::NORMAL); } $this->out(str_repeat('=', 128), 1, Shell::NORMAL); $this->out(sprintf( '| Legend: %s %s %s %s %s |', '█: Warnings', '█: Connectedness', '█: ATT&CK context', '█: Other Context', str_repeat(' ', 52) ), 1, Shell::NORMAL); $this->out(str_repeat('=', 128), 1, Shell::NORMAL); file_put_contents(APP . 'tmp/report.json', json_encode($results, JSON_PRETTY_PRINT)); } }