Exemple #1
0
def _mock_resources(scan_initiator):
    mock_plan_table, mock_info_table = (MagicMock(), MagicMock())
    scan_initiator.dynamo_resource.Table.side_effect = iter([mock_plan_table, mock_info_table])
    scan_initiator.dynamo_resource.close.return_value = coroutine_of(None)
    scan_initiator.ssm_client.close.return_value = coroutine_of(None)
    scan_initiator.sqs_client.close.return_value = coroutine_of(None)
    return mock_info_table, mock_plan_table
async def test_publish_no_data():
    mock_sns_client = MagicMock()
    mock_sns_client.publish.return_value = coroutine_of({"MessageId": "Msg32"})
    context = ResultsContext(
        "PubTopic",
        {"address": "123.123.123.123"},
        "scan_12",
        iso_date_string_from_timestamp(123456),
        iso_date_string_from_timestamp(789123),
        "scan_name",
        mock_sns_client
    )

    await context.publish_results()

    # it should publish the top level info parent and temporal key
    mock_sns_client.publish.assert_called_with(
        TopicArn="PubTopic",
        Subject="scan_name",
        Message=dumps(
            {
                "scan_id": "scan_12",
                "scan_start_time": iso_date_string_from_timestamp(123456),
                "scan_end_time": iso_date_string_from_timestamp(789123),
                "__docs": {}
            }
        ),
        MessageAttributes={
            "ParentKey": {"StringValue": ResultsContext._hash_of({"address": "123.123.123.123"}), "DataType": "String"},
            "TemporalKey": {"StringValue": ResultsContext._hash_of(iso_date_string_from_timestamp(789123)),
                            "DataType": "String"}
        }
    )
def setup_mocks(ssm_failure=False):
    with patch("aioboto3.client"), patch.dict(os.environ, TEST_ENV):
        scanner = NmapScanner()
        scanner.ensure_initialised()
        scanner.ecs_client.run_task.side_effect = (coroutine_of({}) for _ in count(0, 1))
        scanner.ssm_client.get_parameters.return_value = ssm_return_vals(ssm_failure)
        return scanner
    def execute_test_using_results_archive(filename):
        with patch("aioboto3.client"), \
                patch.dict(os.environ, TEST_ENV):
            results_parser = NmapResultsParser()

            results_context_constructor.return_value = mock_results_context = MagicMock()

            mock_mgr = Mock()
            mock_mgr.attach_mock(results_context_constructor, "ResultsContext")
            mock_mgr.attach_mock(mock_results_context.push_context, "push_context")
            mock_mgr.attach_mock(mock_results_context.pop_context, "pop_context")
            mock_mgr.attach_mock(mock_results_context.post_results, "post_results")
            mock_mgr.attach_mock(mock_results_context.publish_results, "publish_results")
            mock_mgr.attach_mock(mock_results_context.add_summaries, "add_summaries")
            mock_mgr.attach_mock(mock_results_context.add_summary, "add_summary")

            results_parser.ensure_initialised()
            results_parser.sns_client.publish.return_value = coroutine_of({"MessageId": "foo"})
            results_parser.ssm_client.get_parameters = MagicMock()
            results_parser.ssm_client.get_parameters.return_value = ssm_return_vals()
            mock_results_context.publish_results.side_effect = (coroutine_of(MagicMock()) for _ in count(0, 1))

            # load sample results file and make mock return it
            sample_file_name = f"{TEST_DIR}{filename}"
            with open(sample_file_name, "rb") as sample_data:
                class AsyncReader:
                    async def read(self):
                        return StreamingBody(sample_data, os.stat(sample_file_name).st_size).read()

                results_parser.s3_client.get_object.return_value = coroutine_of({
                    "Body": AsyncReader()
                })

                results_parser.invoke(
                    {
                        "Records": [
                            {"s3": {
                                "bucket": {"name": "test_bucket"},
                                "object": {"key": filename}
                            }}
                        ]
                    },
                    MagicMock()
                )
            return results_parser, mock_mgr, mock_results_context
Exemple #5
0
def test_paginates_scan_results(_, scan_initiator):
    # ssm params don"t matter much in this test
    set_ssm_return_vals(scan_initiator.ssm_client, 40, 10)

    # access mock for dynamodb table
    mock_info_table, mock_plan_table = _mock_resources(scan_initiator)

    # return a single result but with a last evaluated key present, second result wont have
    # that key
    mock_plan_table.scan.side_effect = iter([
        coroutine_of({
            "Items": [{
                "Address": "123.456.123.456",
                "DnsIngestTime": 12345,
                "PlannedScanTime": 67890
            }],
            "LastEvaluatedKey": "SomeKey"
        }),
        coroutine_of({
            "Items": [{
                "Address": "456.345.123.123",
                "DnsIngestTime": 123456,
                "PlannedScanTime": 67890
            }]
        }),
    ])
    mock_info_table.update_item.side_effect = iter([coroutine_of(None), coroutine_of(None)])

    # pretend the sqs messages are all successfully dispatched
    scan_initiator.sqs_client.send_message_batch.side_effect = [
        coroutine_of(None),
        coroutine_of(None)
    ]
    # pretend the delete item calls are all successful too
    writer = _mock_delete_responses(mock_plan_table, [coroutine_of(None), coroutine_of(None)])

    # actually do the test
    scan_initiator.initiate_scans({}, MagicMock())

    # check the scan happens twice, searching for planned scans earlier than 1984 + 40/10 i.e. now + bucket_length
    assert mock_plan_table.scan.call_args_list == [
        call(
            IndexName="MyIndexName",
            FilterExpression=Key("PlannedScanTime").lte(Decimal(1988))
        ),
        call(
            IndexName="MyIndexName",
            FilterExpression=Key("PlannedScanTime").lte(Decimal(1988)),
            ExclusiveStartKey="SomeKey"
        )
    ]

    # Doesn"t batch across pages
    assert scan_initiator.sqs_client.send_message_batch.call_count == 2
    assert writer.delete_item.call_count == 2
Exemple #6
0
def test_batches_sqs_writes(_, scan_initiator):
    # ssm params don"t matter much in this test
    set_ssm_return_vals(scan_initiator.ssm_client, 100, 4)

    # access mock for dynamodb table
    mock_info_table, mock_plan_table = _mock_resources(scan_initiator)

    # send 32 responses in a single scan result, will be batched into groups of 10 for
    # sqs
    mock_plan_table.scan.side_effect = iter([
        coroutine_of({
            "Items": [
                {
                    "Address": f"123.456.123.{item_num}",
                    "DnsIngestTime": 12345,
                    "PlannedScanTime": 67890
                }
                for item_num in range(0, 32)
            ]
        })
    ])
    mock_info_table.update_item.side_effect = iter([coroutine_of(None) for _ in range(0, 32)])

    # pretend the sqs and dynamo deletes are all ok, there are 4 calls to sqs
    # and
    scan_initiator.sqs_client.send_message_batch.side_effect = [
        coroutine_of(None) for _ in range(0, 4)
    ]
    writer = _mock_delete_responses(mock_plan_table, [
        coroutine_of(None) for _ in range(0, 32)
    ])

    # actually do the test
    scan_initiator.initiate_scans({}, MagicMock())

    # There will be 4 calls to  sqs
    assert scan_initiator.sqs_client.send_message_batch.call_count == 4

    # The last batch will have 2 remaining items in it N.B. a call object is a tuple of the
    # positional args and then the kwags
    assert len(scan_initiator.sqs_client.send_message_batch.call_args_list[3][1]["Entries"]) == 2

    # There will be individual deletes for each address i.e. 32 of them
    assert writer.delete_item.call_count == 32
async def test_summary_info_published():
    mock_sns_client = MagicMock()
    mock_sns_client.publish.return_value = coroutine_of({"MessageId": "Msg32"})
    context = ResultsContext(
        "PubTopic",
        {"address": "123.456.123.456"},
        "scan_9",
        iso_date_string_from_timestamp(4),
        iso_date_string_from_timestamp(5),
        "scan_name",
        mock_sns_client
    )

    context.add_summaries({"foo": "bar", "boo": "baz"})
    context.add_summary("banana", "man")
    context.post_results("host_info", {"uptime": "1234567"}, include_summaries=True)
    await context.publish_results()

    mock_sns_client.publish.assert_called_with(
        TopicArn="PubTopic",
        Subject="scan_name",
        Message=dumps(
            {
                "scan_id": "scan_9",
                "scan_start_time": iso_date_string_from_timestamp(4),
                "scan_end_time": iso_date_string_from_timestamp(5),
                "__docs": {
                    "host_info": [
                        {
                            "NonTemporalKey": ResultsContext._hash_of({
                                "address": "123.456.123.456",
                            }),
                            "Data": {
                                "address": "123.456.123.456",
                                "uptime": "1234567",
                                "summary_foo": "bar",
                                "summary_boo": "baz",
                                "summary_banana": "man",
                                "__ParentKey": ResultsContext._hash_of({"address": "123.456.123.456"}),
                            }
                        }
                    ]
                }
            }
        ),
        MessageAttributes={
            "ParentKey": {
                "StringValue": ResultsContext._hash_of({"address": "123.456.123.456"}),
                "DataType": "String"
            },
            "TemporalKey": {
                "StringValue": ResultsContext._hash_of(iso_date_string_from_timestamp(5)),
                "DataType": "String"
            }
        }
    )
Exemple #8
0
def ssm_return_vals():
    stage = os.environ["STAGE"]
    app_name = os.environ["APP_NAME"]
    ssm_prefix = f"/{app_name}/{stage}"
    return coroutine_of({
        "Parameters": [{
            "Name": f"{ssm_prefix}/analytics/elastic/es_endpoint/url",
            "Value": "elastic.url.com"
        }]
    })
 def test_raises_when_ecs_failures_present():
     with patch("aioboto3.client"), patch.dict(os.environ, TEST_ENV):
         scanner = NmapScanner()
         scanner.ensure_initialised()
         scanner.ssm_client.get_parameters.return_value = ssm_return_vals(False)
         scanner.ecs_client.run_task.return_value = coroutine_of({"failures": [
             {"arn": "arn::some:::arn", "reason": "failed miserably"}
         ]})
         with pytest.raises(RuntimeError, match=r"\{\"arn\": \"arn::some:::arn\", \"reason\": \"failed miserably\"\}"):
             scanner.invoke({"Records": [{"body": "some.host", "messageId": "13"}]}, MagicMock())
 def ssm_return_vals():
     stage = os.environ["STAGE"]
     app_name = os.environ["APP_NAME"]
     task_name = os.environ["TASK_NAME"]
     ssm_prefix = f"/{app_name}/{stage}"
     return coroutine_of({
         "Parameters": [
             {"Name": f"{ssm_prefix}/tasks/{task_name}/results/arn", "Value": "test_topic_arn"},
             {"Name": f"{ssm_prefix}/tasks/{task_name}/s3/results/id", "Value": "test_topic_id"}
         ]
     })
Exemple #11
0
def set_ssm_return_vals(ssm_client, period, buckets):
    stage = os.environ["STAGE"]
    app_name = os.environ["APP_NAME"]
    ssm_prefix = f"/{app_name}/{stage}"

    ssm_client.get_parameters.return_value = coroutine_of({
            "Parameters": [
                {"Name": f"{ssm_prefix}/scheduler/dynamodb/scans_planned/id", "Value": "MyTableId"},
                {"Name": f"{ssm_prefix}/scheduler/dynamodb/scans_planned/plan_index", "Value": "MyIndexName"},
                {"Name": f"{ssm_prefix}/scheduler/dynamodb/address_info/id", "Value": "MyIndexName"},
                {"Name": f"{ssm_prefix}/scheduler/config/period", "Value": str(period)},
                {"Name": f"{ssm_prefix}/scheduler/config/buckets", "Value": str(buckets)},
                {"Name": f"{ssm_prefix}/scheduler/scan_delay_queue", "Value": "MyDelayQueue"}
            ]
        })
def ssm_return_vals(using_private):
    stage = os.environ["STAGE"]
    app_name = os.environ["APP_NAME"]
    task_name = os.environ["TASK_NAME"]
    ssm_prefix = f"/{app_name}/{stage}"
    return coroutine_of({
        "Parameters": [
            {"Name": f"{ssm_prefix}/vpc/using_private_subnets", "Value": "true" if using_private else "false"},
            {"Name": f"{ssm_prefix}/tasks/{task_name}/security_group/id", "Value": "sg-123"},
            {"Name": f"{ssm_prefix}/tasks/{task_name}/image/id", "Value": "imagination"},
            {"Name": f"{ssm_prefix}/tasks/{task_name}/s3/results/id", "Value": "bid"},
            {"Name": f"{ssm_prefix}/vpc/subnets/instance", "Value": "subnet-123,subnet-456"},
            {"Name": f"{ssm_prefix}/ecs/cluster", "Value": "cid"}
        ]
    })
Exemple #13
0
def test_replace_punctuation_in_address_ids(_, scan_initiator):
    # ssm params don"t matter much in this test
    set_ssm_return_vals(scan_initiator.ssm_client, 100, 4)

    # access mock for dynamodb table
    mock_info_table, mock_plan_table = _mock_resources(scan_initiator)

    # return a single result with ip4 and another with ip6
    mock_plan_table.scan.side_effect = iter([
        coroutine_of({
            "Items": [
                {
                    "Address": "123.456.123.456",
                    "DnsIngestTime": 12345,
                    "PlannedScanTime": 67890
                },
                {
                    "Address": "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
                    "DnsIngestTime": 12345,
                    "PlannedScanTime": 67890
                }
            ]
        })
    ])
    mock_info_table.update_item.side_effect = iter([coroutine_of(None), coroutine_of(None)])

    # pretend the sqs and dynamo deletes are all ok
    scan_initiator.sqs_client.send_message_batch.side_effect = [coroutine_of(None)]
    _mock_delete_responses(mock_plan_table, [coroutine_of(None), coroutine_of(None)])

    # actually do the test
    scan_initiator.initiate_scans({}, MagicMock())

    # check both addresses have : and . replaced with -
    scan_initiator.sqs_client.send_message_batch.assert_called_once_with(
        QueueUrl="MyDelayQueue",
        Entries=[
            {
                "Id": "123-456-123-456",
                "DelaySeconds": 67890-1984,  # planned scan time minus now time
                "MessageBody": "{\"AddressToScan\":\"123.456.123.456\"}"
            },
            {
                "Id": "2001-0db8-85a3-0000-0000-8a2e-0370-7334",
                "DelaySeconds": 67890-1984,  # planned scan time minus now time
                "MessageBody": "{\"AddressToScan\":\"2001:0db8:85a3:0000:0000:8a2e:0370:7334\"}"
            }
        ]
    )
Exemple #14
0
def test_no_deletes_until_all_sqs_success(_, scan_initiator):
    # ssm params don"t matter much in this test
    set_ssm_return_vals(scan_initiator.ssm_client, 100, 4)

    # access mock for dynamodb table
    mock_info_table, mock_plan_table = _mock_resources(scan_initiator)

    # send a single response
    mock_plan_table.scan.side_effect = [
        coroutine_of({
            "Items": [
                {
                    "Address": f"123.456.123.5",
                    "DnsIngestTime": 12345,
                    "PlannedScanTime": 67890
                }
            ]
        })
    ]

    # pretend the sqs and dynamo deletes are all ok, there are 4 calls to sqs
    # and
    scan_initiator.sqs_client.send_message_batch.side_effect = [
        Exception("test error")
    ]
    writer = _mock_delete_responses(mock_plan_table, [])

    # actually do the test
    with pytest.raises(Exception):
        scan_initiator.initiate_scans({}, MagicMock())

    # There will be 1 call to sqs
    assert scan_initiator.sqs_client.send_message_batch.call_count == 1

    # and none to dynamo
    assert writer.delete_item.call_count == 0
async def test_context_push_and_pop():
    mock_sns_client = MagicMock()
    mock_sns_client.publish.return_value = coroutine_of({"MessageId": "Msg32"})
    context = ResultsContext(
        "PubTopic",
        {"address": "123.456.123.456"},
        "scan_2",
        iso_date_string_from_timestamp(4),
        iso_date_string_from_timestamp(5),
        "scan_name",
        mock_sns_client
    )

    context.push_context({"port": "22"})
    context.post_results("port_info", {"open": "false"})
    context.push_context({"vulnerability": "cve4"})
    context.post_results("vuln_info", {"severity": "5"})
    context.pop_context()
    context.push_context({"vulnerability": "cve5"})
    context.post_results("vuln_info", {"severity": "2"})
    context.pop_context()
    context.pop_context()
    context.push_context({"port": "80"})
    context.post_results("port_info", {"open": "true"})
    context.pop_context()
    context.post_results("host_info", {"uptime": "1234567"})
    await context.publish_results()

    # it should publish the top level info parent and temporal key
    mock_sns_client.publish.assert_called_with(
        TopicArn="PubTopic",
        Subject="scan_name",
        Message=dumps(
            {
                "scan_id": "scan_2",
                "scan_start_time": iso_date_string_from_timestamp(4),
                "scan_end_time": iso_date_string_from_timestamp(5),
                "__docs": {
                    "port_info": [
                        {
                            "NonTemporalKey": ResultsContext._hash_of({
                                "address": "123.456.123.456",
                                "port": "22"
                            }),
                            "Data": {
                                "address": "123.456.123.456",
                                "port": "22",
                                "open": "false",
                                "__ParentKey": ResultsContext._hash_of({"address": "123.456.123.456"}),
                            }
                        },
                        {
                            "NonTemporalKey": ResultsContext._hash_of({
                                "address": "123.456.123.456",
                                "port": "80"
                            }),
                            "Data": {
                                "address": "123.456.123.456",
                                "port": "80",
                                "open": "true",
                                "__ParentKey": ResultsContext._hash_of({"address": "123.456.123.456"}),
                            }
                        }
                    ],
                    "vuln_info": [
                        {
                            "NonTemporalKey": ResultsContext._hash_of({
                                "address": "123.456.123.456",
                                "port": "22",
                                "vulnerability": "cve4"
                            }),
                            "Data": {
                                "address": "123.456.123.456",
                                "port": "22",
                                "vulnerability": "cve4",
                                "severity": "5",
                                "__ParentKey": ResultsContext._hash_of({"address": "123.456.123.456"}),
                            }
                        },
                        {
                            "NonTemporalKey": ResultsContext._hash_of({
                                "address": "123.456.123.456",
                                "port": "22",
                                "vulnerability": "cve5"
                            }),
                            "Data": {
                                "address": "123.456.123.456",
                                "port": "22",
                                "vulnerability": "cve5",
                                "severity": "2",
                                "__ParentKey": ResultsContext._hash_of({"address": "123.456.123.456"}),
                            }
                        }
                    ],
                    "host_info": [
                        {
                            "NonTemporalKey": ResultsContext._hash_of({
                                "address": "123.456.123.456",
                            }),
                            "Data": {
                                "address": "123.456.123.456",
                                "uptime": "1234567",
                                "__ParentKey": ResultsContext._hash_of({"address": "123.456.123.456"}),
                            }
                        }
                    ]
                }
            }
        ),
        MessageAttributes={
            "ParentKey": {
                "StringValue": ResultsContext._hash_of({"address": "123.456.123.456"}),
                "DataType": "String"
            },
            "TemporalKey": {
                "StringValue": ResultsContext._hash_of(iso_date_string_from_timestamp(5)),
                "DataType": "String"
            }
        }
    )