项目作者: mozilla-services

项目描述 :
用于AWS配置的测试驱动安全性的单元测试框架等。
高级语言: Python
项目地址: git://github.com/mozilla-services/pytest-services.git
创建时间: 2017-10-04T20:00:35Z
项目社区:https://github.com/mozilla-services/pytest-services

开源协议:Mozilla Public License 2.0

下载


Frost

PyPI version
Documentation

frost snowman logo

HTTP clients and a wrapper around
pytest tests to verify
that third party services are configured correctly. For example:

  • Are our AWS DB snapshots publicly accessible?
  • Are there dangling DNS entries in Route53?

Usage

Installing

  1. Install Python 3.8
  2. Run git clone git@github.com:mozilla/frost.git; cd frost; make install

Usage

  1. $ frost --help
  2. Usage: frost [OPTIONS] COMMAND [ARGS]...
  3. FiRefox Operations Security Testing API clients and tests
  4. Options:
  5. --version Show the version and exit.
  6. --help Show this message and exit.
  7. Commands:
  8. list Lists available test filenames packaged with frost.
  9. test Run pytest tests passing all trailing args to pytest.
  10. $ frost test --help
  11. Usage: frost test [OPTIONS] [PYTEST_ARGS]...
  12. Run pytest tests passing all trailing args to pytest.
  13. Adds the pytest args:
  14. -s to disable capturing stdout
  15. https://docs.pytest.org/en/latest/capture.html
  16. and frost specific arg:
  17. --debug-calls to print AWS API calls
  18. Options:
  19. --help Show this message and exit.

Running

To fetch RDS resources from the cache or AWS API and check that
backups are enabled for DB instances for the configured aws
profile

named default in the us-west-2 region

  1. find the test file path:
  1. $ frost list | grep rds
  2. ./aws/rds/test_rds_db_instance_backup_enabled.py
  3. ./aws/rds/test_rds_db_snapshot_encrypted.py
  4. ./aws/rds/test_rds_db_instance_is_postgres_with_invalid_certificate.py
  5. ./aws/rds/test_rds_db_instance_encrypted.py
  6. ./aws/rds/test_rds_db_security_group_does_not_grant_public_access.py
  7. ./aws/rds/test_rds_db_instance_not_publicly_accessible_by_vpc_sg.py
  8. ./aws/rds/test_rds_db_instance_minor_version_updates_enabled.py
  9. ./aws/rds/test_rds_db_instance_is_multiaz.py
  10. ./aws/rds/test_rds_db_snapshot_not_publicly_accessible.py

Note: packaged frost tests are relative to the frost install

  1. run the test:
  1. frost test aws/rds/test_rds_db_instance_backup_enabled.py --aws-profiles default

Frost adds the options:

  • --aws-profiles for selecting one or more AWS profiles to fetch resources for or the AWS default profile / AWS_PROFILE environment variable
  • --aws-regions for selecting one or more AWS regions to test as a CSV e.g. us-east-1,us-west-2. defaults to all regions
  • --gcp-project-id for selecting the GCP project to test. Required for GCP tests
  • --offline a flag to tell HTTP clients to not make requests and return empty params
  • --config path to test custom config file

and produces output showing calls to the AWS API and failing for a DB
instance with backups disabled:

  1. ============================================================ test session starts ============================================================
  2. platform linux -- Python 3.8.2, pytest-6.0.2, py-1.9.0, pluggy-0.13.1
  3. rootdir: /home/gguthe/frost
  4. plugins: json-0.4.0, cov-2.10.0, html-1.20.0, metadata-1.10.0
  5. collecting ... calling AWSAPICall(profile='default, region='ap-northeast-1', service='rds', method='describe_db_instances', args=[], kwargs={})
  6. calling AWSAPICall(profile='default, region='ap-northeast-2', service='rds', method='describe_db_instances', args=[], kwargs={})
  7. calling AWSAPICall(profile='default, region='ap-south-1', service='rds', method='describe_db_instances', args=[], kwargs={})
  8. calling AWSAPICall(profile='default, region='ap-southeast-1', service='rds', method='describe_db_instances', args=[], kwargs={})
  9. calling AWSAPICall(profile='default, region='ap-southeast-2', service='rds', method='describe_db_instances', args=[], kwargs={})
  10. ...
  11. calling AWSAPICall(profile='default, region='us-west-2', service='rds', method='list_tags_for_resource', args=[], kwargs={'ResourceName': 'arn:aws:rds:us-west-2:redacted:db:test-db-ro-dev1'})
  12. collected 21 items
  13. aws/rds/test_rds_db_instance_backup_enabled.py F....................
  14. ================================================================= FAILURES ==================================================================
  15. ____________________________________ test_rds_db_instance_backup_enabled[test-db-ro-dev1] ___________________________________________________
  16. rds_db_instance = {'AllocatedStorage': 250, 'AutoMinorVersionUpgrade': True, 'AvailabilityZone': 'us-east-1a', 'BackupRetentionPeriod': 0, ..
  17. .}
  18. @pytest.mark.rds
  19. @pytest.mark.parametrize(
  20. "rds_db_instance", rds_db_instances_with_tags(), ids=get_db_instance_id,
  21. )
  22. def test_rds_db_instance_backup_enabled(rds_db_instance):
  23. > assert (
  24. rds_db_instance["BackupRetentionPeriod"] > 0
  25. ), "Backups disabled for {}".format(rds_db_instance["DBInstanceIdentifier"])
  26. E AssertionError: Backups disabled for test-db-ro-dev1
  27. E assert 0 > 0
  28. aws/rds/test_rds_db_instance_backup_enabled.py:12: AssertionError
  29. ========================================================== short test summary info ==========================================================
  30. FAILED aws/rds/test_rds_db_instance_backup_enabled.py::test_rds_db_instance_backup_enabled[test-db-ro-dev1] - AssertionError...
  31. ======================================================= 2 failed, 21 passed in 14.32s =======================================================

IAM Policy for frost

The below policy will allow you to run all AWS tests in frost against all resources in your account.

  1. {
  2. "Version": "2012-10-17",
  3. "Statement": [
  4. {
  5. "Sid": "PytestServicesReadOnly",
  6. "Action": [
  7. "autoscaling:DescribeLaunchConfigurations",
  8. "cloudtrail:DescribeTrails",
  9. "ec2:DescribeFlowLogs",
  10. "ec2:DescribeInstances",
  11. "ec2:DescribeSecurityGroups",
  12. "ec2:DescribeSnapshotAttribute",
  13. "ec2:DescribeSnapshots",
  14. "ec2:DescribeVolumes",
  15. "ec2:DescribeVpcs",
  16. "elasticache:DescribeCacheClusters",
  17. "elasticloadbalancing:DescribeLoadBalancers",
  18. "es:DescribeElasticsearchDomains",
  19. "es:ListDomainNames",
  20. "iam:GenerateCredentialReport",
  21. "iam:GetCredentialReport",
  22. "iam:GetLoginProfile",
  23. "iam:ListAccessKeys",
  24. "iam:ListAttachedGroupPolicies",
  25. "iam:ListAttachedRolePolicies",
  26. "iam:ListAttachedUserPolicies",
  27. "iam:ListGroupPolicies",
  28. "iam:ListGroupsForUser",
  29. "iam:ListMFADevices",
  30. "iam:ListRolePolicies",
  31. "iam:ListRoles",
  32. "iam:ListUserPolicies",
  33. "iam:ListUsers",
  34. "rds:DescribeDbInstances",
  35. "rds:DescribeDbSecurityGroups",
  36. "rds:DescribeDbSnapshotAttributes",
  37. "rds:DescribeDbSnapshots",
  38. "rds:ListTagsForResource",
  39. "redshift:DescribeClusterSecurityGroups",
  40. "redshift:DescribeClusters",
  41. "s3:GetBucketAcl",
  42. "s3:GetBucketCORS",
  43. "s3:GetBucketLogging",
  44. "s3:GetBucketPolicy",
  45. "s3:GetBucketVersioning",
  46. "s3:GetBucketWebsite",
  47. "s3:ListAllMyBuckets",
  48. "s3:ListBucket"
  49. ],
  50. "Effect": "Allow",
  51. "Resource": "*"
  52. }
  53. ]
  54. }

Setting up GCP tests

Enabling required API’s for your project
  1. gcloud [--project <project name>] services enable bigquery-json.googleapis.com
  2. gcloud [--project <project name>] services enable cloudresourcemanager.googleapis.com
  3. gcloud [--project <project name>] services enable compute.googleapis.com
  4. gcloud [--project <project name>] services enable sqladmin.googleapis.com

Setting up GSuite tests

Make sure to have an OAuth2 app created and have the client_secret.json file in ~/.credentials and then run:

  1. make setup_gsuite

Caching

The AWS client will use AWS API JSON responses when available and save them using AWS profile, region, service name, service method, botocore args and kwargs in the cache key to filenames with the format .cache/v/pytest_aws:<aws profile>:<aws region>:<aws service>:<service method>:<args>:<kwargs>.json e.g.

  1. head .cache/v/pytest_aws:cloudservices-aws-stage:us-west-2:rds:describe_db_instances::.json
  2. {
  3. "DBInstances": [
  4. {
  5. "AllocatedStorage": 5,
  6. "AutoMinorVersionUpgrade": true,
  7. "AvailabilityZone": "us-west-2c",
  8. "BackupRetentionPeriod": 1,
  9. "CACertificateIdentifier": "rds-ca-2015",
  10. "CopyTagsToSnapshot": false,
  11. "DBInstanceArn": "arn:aws:rds:us-west-2:123456678901:db:test-db",

These files can be removed individually or all at once with the pytest —cache-clear option.
The cache can be disabled entirely with the pytest -p no:cacheprovider.

Custom Test Config

frost adds a --config cli option for passing in a custom config file specific to tests within frost.

The example config in repo (config.yaml.example):

  1. exemptions:
  2. - test_name: test_ec2_instance_has_required_tags
  3. test_param_id: i-0123456789f014c162
  4. expiration_day: 2019-01-01
  5. reason: ec2 instance has no owner
  6. - test_name: test_ec2_security_group_opens_specific_ports_to_all
  7. test_param_id: '*HoneyPot'
  8. expiration_day: 2020-01-01
  9. reason: purposefully insecure security group
  10. severities:
  11. - test_name: test_ec2_instance_has_required_tags
  12. severity: INFO
  13. - test_name: '*'
  14. severity: ERROR
  15. regressions:
  16. - test_name: test_ec2_security_group_opens_all_ports_to_all
  17. test_param_id: '*mycustomgroup'
  18. comment: this was remediated by ops team
  19. aws:
  20. admin_groups:
  21. - "Administrators"
  22. admin_policies:
  23. - "AWSAdminRequireMFA"
  24. user_is_inactive:
  25. no_activity_since:
  26. years: 1
  27. months: 0
  28. created_after:
  29. weeks: 1
  30. access_key_expires_after:
  31. years: 1
  32. months: 0
  33. required_tags:
  34. - Name
  35. - Type
  36. - App
  37. - Env
  38. required_amis:
  39. - ami-00000000000000000
  40. - ami-55555555555555555
  41. # Allowed ports for the test_ec2_security_group_opens_specific_ports_to_all
  42. # test for all instances
  43. allowed_ports_global:
  44. - 25
  45. # Allowed ports for the test_ec2_security_group_opens_specific_ports_to_all
  46. # test for specific instances. In this example, we are allowing ports 22
  47. # and 2222 for all security groups that include the word 'bastion' in them.
  48. allowed_ports:
  49. - test_param_id: '*bastion'
  50. ports:
  51. - 22
  52. - 2222
  53. gcp:
  54. allowed_org_domains:
  55. - mygsuiteorg.com
  56. allowed_gke_versions:
  57. - 1.15.12-gke.20
  58. - 1.16.13-gke.401
  59. - 1.17.9-gke.1504
  60. - 1.18.6-gke.3504
  61. # Allowed ports for the test_firewall_opens_any_ports_to_all
  62. # test for all firewalls
  63. allowed_ports_global:
  64. - 25
  65. # Allowed ports for the test_firewall_opens_any_ports_to_all
  66. # test for specific firewalls. In this example, we are allowing ports 22
  67. # and 2222 for all firewalls that include the word 'bastion' in them.
  68. allowed_ports:
  69. - test_param_id: '*bastion'
  70. ports:
  71. - 22
  72. - 2222
  73. gsuite:
  74. domain: 'example.com'
  75. user_is_inactive:
  76. no_activity_since:
  77. years: 1
  78. months: 0

Test Exemptions

frost custom config format adds support for
marking test and test resource IDs as expected failures.

The keys for each exemption rule is:

  • test_name - Name of the test
  • test_param_id - test ID (usually an AWS resource ID) (prefix with * to turn into a regex matcher)
  • expiration_day - exception expiration day (as YYYY-MM-DD)
  • reason - exception reason

The config looks like:

  1. ...
  2. exemptions:
  3. - test_name: test_ec2_instance_has_required_tags
  4. test_param_id: i-0123456789f014c162
  5. expiration_day: 2019-01-01
  6. reason: ec2 instance has no owner
  7. - test_name: test_ec2_security_group_opens_specific_ports_to_all
  8. test_param_id: '*HoneyPot'
  9. expiration_day: 2020-01-01
  10. reason: purposefully insecure security group
  11. ...

Enabling regex for test ID

You can prefix the test ID with a * to enable regex matching for the test ID. The * prefix will be stripped
off, and the rest will be used as a regex.

For example:

  • *foobar becomes foobar
  • *foo\w+ becomes foo\w+

For more information on Python’s regex syntax see: Regular Expression HOWTO.

Note: All regex rules are applied first. As well, the ordering of both regex and non-regex rules is top to bottom and the first one wins.

When a json report is generated, the exemptions will show up in the
json metadata as serialized markers:

  1. python -m json.tool report.json | grep -C 20 xfail
  2. ...
  3. "markers": {
  4. "ec2": {
  5. "name": "ec2",
  6. "args": [],
  7. "kwargs": {}
  8. },
  9. "parametrize": {
  10. "name": "parametrize",
  11. "args": [
  12. "...skipped..."
  13. ],
  14. "kwargs": [
  15. "...skipped..."
  16. ]
  17. },
  18. "xfail": {
  19. "name": "xfail",
  20. "args": [],
  21. "kwargs": {
  22. "reason": "ec2 instance has no owner",
  23. "strict": true,
  24. "expiration": "2019-01-01"
  25. }
  26. }
  27. },
  28. ...

Test Severity

frost custom config format adds support for marking the severity of a certain test. A severity can be INFO, WARN, or ERROR.

These do not modify pytest results (pass, fail, xfail, skip, etc.).

The config looks like:

  1. ...
  2. severities:
  3. - test_name: test_ec2_instance_has_required_tags
  4. severity: INFO
  5. - test_name: '*'
  6. severity: ERROR
  7. ...

And results in a severity and severity marker being included in the
json metadata:

  1. frost test -s --aws-profiles stage --aws-require-tags Name Type App Stack -k test_ec2_instance_has_required_tags --config config.yaml.example --json=report.json
  2. ...
  1. python -m json.tool report.json
  2. {
  3. "report": {
  4. "environment": {
  5. "Python": "3.6.2",
  6. "Platform": "Darwin-15.6.0-x86_64-i386-64bit"
  7. },
  8. "tests": [
  9. {
  10. ...
  11. "metadata": [
  12. {
  13. ...
  14. "markers": {
  15. ...
  16. "severity": {
  17. "name": "severity",
  18. "args": [
  19. "INFO"
  20. ],
  21. "kwargs": {}
  22. }
  23. },
  24. ...
  25. "severity": "INFO",
  26. "unparametrized_name": "test_ec2_instance_has_required_tags"
  27. }
  28. ...

AWS Config

frost has a suite of AWS tests. This section of the custom config includes configuration options specific
to these tests.

The config looks like:

  1. ...
  2. aws:
  3. # Relative time delta for test_iam_user_is_inactive. no_activity_since will be used as the failure marker,
  4. # so in this example any user that hasn't had any activity for a year will be marked as a "failure". created_after
  5. # is used as a grace period, so in this case any user that was created within the last week will be automatically
  6. # pass this test.
  7. user_is_inactive:
  8. no_activity_since:
  9. years: 1
  10. months: 0
  11. created_after:
  12. weeks: 1
  13. # Required tags used within the test_ec2_instance_has_required_tags test
  14. required_tags:
  15. - Name
  16. - Type
  17. - App
  18. - Env
  19. # Allowed ports for the test_ec2_security_group_opens_specific_ports_to_all
  20. # test for all instances
  21. allowed_ports_global:
  22. - 25
  23. # Allowed ports for the test_ec2_security_group_opens_specific_ports_to_all
  24. # test for specific instances. In this example, we are allowing ports 22
  25. # and 2222 for all security groups that include the word 'bastion' in them.
  26. allowed_ports:
  27. - test_param_id: '*bastion'
  28. ports:
  29. - 22
  30. - 2222
  31. ...

GSuite Config

frost has a suite of GSuite tests. This section of the
custom config includes configuration options specific to these tests.

Make sure to setup GSuite before running GSuite tests

The config looks like:

  1. gsuite:
  2. # The specific GSuite domain to test.
  3. domain: 'example.com'
  4. # Relative time delta for test_admin_user_is_inactive. no_activity_since will be used as the failure marker,
  5. # so in this example any user that hasn't had any activity for a year will be marked as a "failure".
  6. user_is_inactive:
  7. no_activity_since:
  8. years: 1
  9. months: 0

Test Accuracy

There are two important things to note about frost tests that may be different from your expectations.

First, the focus is on “actionable results”. This plays out as an attempt to reduce false
positives by trying to filter out unused resources. An example of this can be seen by looking at
any of the security group tests, where we are skipping any security groups that are not attached to a resource.

Second, there are some tests that make naive assumptions instead of trying to capture the complexities
of the system. The current best example of this is all IAM tests that relate to “admin” users. How we
are determining what an user or role is an admin is based simply off substring matching on the policies
attached. This obviously has a high chance of false negatives.

Development

Goals

  1. replace one-off scripts for each check
  2. share checks with other organizations
  3. consolidate bugs in one place (i.e. one thing to update)
  4. in pytest use a known existing framework for writing checks
  5. be vendor agnostic e.g. support checks across cloud providers or in hybrid environments or competing services
  6. cache and share responses to reduce third party API usage (i.e. lots of tests check AWS security groups so fetch them once)
  7. provide a way to run a single test or subset of tests
  8. focus on actionable results (see test accuracy for more information)

Non-Goals

  1. Invent a new DSL for writing expectations (use pytest conventions)
  2. Verify how third party services or their client libraries work
    (e.g. don’t answer “Does GET / on the CRUD1 API return 400 when
    query param q is $bad_value?”)

Design

Currently this is a monolithic pytest package, but should eventually
be extracted into a pytest plugin and with separate dependent
pytest plugins for each service
.

API responses should fit on disk and in memory (i.e. don’t use this
for log processing or checking binaries for malware), and be safe to
cache for minutes, hours, or days (i.e. probably don’t use this for
monitoring a streaming API) (NB: bug for specifying data
freshness
).

Additionally we want:

  • data fetching functions in a resources.py
  • data checking and test helpers in a helpers.py
  • prefix test files with test_
  • doctests for non test files (e.g. helpers.py, resources.py, client.py)
    • tests that depend on external IO or the runtime environment (env vars, file system, HTTP) to use the prefix meta_test_ (and probably mock or pytest.monkeypatch)
      • JSON fixtures for anonymized cached http call in example_cache/v/
  • tests to have pytest markers for any services they depend on for data
  • HTTP clients should be read only and use read only credentials
  • running a test should not modify services

File Layout

  1. frost
  2. ...
  3. ├── example_cache
  4. └── v
  5. ├── cache
  6. └── lastfailed
  7. ├── pytest_aws:example-account:us-east-1:ec2:describe_instances::.json
  8. ├── pytest_aws:example-account:us-east-1:ec2:describe_security_groups::.json
  9. ...
  10. ├── <third party service A>
  11. ├── client.py
  12. ├── conftest.py
  13. ├── meta_test_client.py
  14. ├── <subservice A (optional)>
  15. ├── __init__.py
  16. ├── helpers.py
  17. ├── resources.py
  18. ├── ...
  19. └── test_ec2_security_group_all_ports.py
  20. ├── <subservice b (optional)>
  21. ├── __init__.py
  22. ├── resources.py
  23. ├── ...
  24. └─ test_s3_bucket_web_hosting_disabled.py
  25. └── <third party service B>
  26. ├── __init__.py
  27. ├── conftest.py
  28. ├── helpers.py
  29. ├── resources.py
  30. └── test_user_has_escalation_policy.py

Adding an example test

Let’s write a test to check that http://httpbin.org/ip returns an AWS IP:

  1. create a file httpbin/test_httpbin_ip.py with the contents:
  1. import itertools
  2. import ipaddress
  3. import pytest
  4. import json
  5. import urllib.request
  6. def get_httpbin_ips():
  7. # IPs we always want to test
  8. ips = [
  9. '127.0.0.1',
  10. '13.58.0.0',
  11. ]
  12. req = urllib.request.Request('http://httpbin.org/ip')
  13. with urllib.request.urlopen(req) as response:
  14. body = response.read().decode('utf-8')
  15. ips.append(json.loads(body).get('origin', None))
  16. return ips
  17. def get_aws_ips():
  18. req = urllib.request.Request('https://ip-ranges.amazonaws.com/ip-ranges.json')
  19. with urllib.request.urlopen(req) as response:
  20. body = response.read().decode('utf-8')
  21. return json.loads(body)['prefixes']
  22. @pytest.mark.httpbin
  23. @pytest.mark.aws_ip_ranges
  24. @pytest.mark.parametrize(
  25. ['ip', 'aws_ip_ranges'],
  26. zip(get_httpbin_ips(), itertools.repeat(get_aws_ips())))
  27. def test_httpbin_ip_in_aws(ip, aws_ip_ranges):
  28. for aws_ip_range in aws_ip_ranges:
  29. assert ipaddress.IPv4Address(ip) not in ipaddress.ip_network(aws_ip_range['ip_prefix']), \
  30. "{0} is in AWS range {1[ip_prefix]} region {1[region]} service {1[service]}".format(ip, aws_ip_range)

Notes:

  • we add two data fetching functions that return lists that we can zip into tuples for the pytest parametrize decorator
  • we add markers for the services we’re fetching data from
  1. Running frost test with the test file explicitly included we see that one of the IPs is an AWS IP:
  1. frost test httpbin/test_httpbin_ip_in_aws.py
  2. platform darwin -- Python 3.6.2, pytest-3.3.2, py-1.5.2, pluggy-0.6.0
  3. metadata: {'Python': '3.6.2', 'Platform': 'Darwin-15.6.0-x86_64-i386-64bit', 'Packages': {'pytest': '3.3.2', 'py': '1.5.2', 'pluggy': '0.6.0'}, 'Plugins': {'metadata': '1.5.1', 'json': '0.4.0', 'html': '1.16.1'}}
  4. rootdir: /Users/gguthe/frost, inifile:
  5. plugins: metadata-1.5.1, json-0.4.0, html-1.16.1
  6. collected 3 items
  7. httpbin/test_httpbin_ip_in_aws.py .F. [100%]
  8. ================================================================ FAILURES =================================================================
  9. ____________________________________________ test_httpbin_ip_in_aws[13.58.0.0-aws_ip_ranges1] _____________________________________________
  10. ip = '13.58.0.0'
  11. aws_ip_ranges = [{'ip_prefix': '13.32.0.0/15', 'region': 'GLOBAL', 'service': 'AMAZON'}, {'ip_prefix': '13.35.0.0/16', 'region': 'GLOB...on': 'us-west-1', 'service': 'AMAZON'}, {'ip_prefix': '13.57.0.0/16', 'region': 'us-west-1', 'service': 'AMAZON'}, ...]
  12. @pytest.mark.httpbin
  13. @pytest.mark.aws_ip_ranges
  14. @pytest.mark.parametrize(
  15. ['ip', 'aws_ip_ranges'],
  16. zip(get_httpbin_ips(), itertools.repeat(get_aws_ips())),
  17. # ids=lambda ip: ip
  18. )
  19. def test_httpbin_ip_in_aws(ip, aws_ip_ranges):
  20. for aws_ip_range in aws_ip_ranges:
  21. > assert ipaddress.IPv4Address(ip) not in ipaddress.ip_network(aws_ip_range['ip_prefix']), \
  22. "{0} is in AWS range {1[ip_prefix]} region {1[region]} service {1[service]}".format(ip, aws_ip_range)
  23. E AssertionError: 13.58.0.0 is in AWS range 13.58.0.0/15 region us-east-2 service AMAZON
  24. E assert IPv4Address('13.58.0.0') not in IPv4Network('13.58.0.0/15')
  25. E + where IPv4Address('13.58.0.0') = <class 'ipaddress.IPv4Address'>('13.58.0.0')
  26. E + where <class 'ipaddress.IPv4Address'> = ipaddress.IPv4Address
  27. E + and IPv4Network('13.58.0.0/15') = <function ip_network at 0x107cf66a8>('13.58.0.0/15')
  28. E + where <function ip_network at 0x107cf66a8> = ipaddress.ip_network
  29. httpbin/test_httpbin_ip_in_aws.py:43: AssertionError
  30. =================================================== 1 failed, 2 passed in 15.69 seconds ===================================================

Note: marking tests as expected failures with @pytest.mark.xfail can hide data fetching errors

To improve this we could:

  1. Add parametrize ids so it’s clearer which parametrize caused test failures
  2. Add directions about why it’s an issue and how to fix it or what the associated risks are

As we add more tests we can:

  1. Move the JSON fetching functions to <service name>/resources.py files and import them into the test
  2. Move the fetching logic to a shared library <service name>/client.py and save to the pytest cache
  3. Add a <service name>/conftest.py and register the service’s marks in a pytest_configure to resolve some warnings