Setup initial CI gating on tests and add linting tests (#85)

* add flake, black, mypy, and bandit to run

* fixes issues flake and black complained about

* make mypy run successfully, add configuration files rather than specifying in ci

* respond to feedback

* configure bandit, ignore a file used only in local development
This commit is contained in:
Logan McDonald 2022-08-26 12:36:02 -04:00 committed by GitHub
parent e288578028
commit 8f41050f76
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 124 additions and 71 deletions

35
.github/workflows/test.yaml vendored Normal file
View file

@ -0,0 +1,35 @@
# test.yml
name: Testing
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
python-linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v3
- name: Install linters
run: |
python -m pip install --upgrade pip
pip install bandit black flake8 mypy types-requests
- name: Lint with flake8
working-directory: ./src
run: flake8 . --count --show-source --statistics
- name: Check formatting with black
working-directory: ./src
run: black --check .
- name: Run type checking
working-directory: ./src
run: mypy .
- name: Run bandit security scanning
working-directory: ./src
run: bandit -r .

View file

@ -11,25 +11,29 @@ NOTE: This requries python-whois and argparse to be installed.
import csv import csv
import requests import requests
import whois # this is python-whois import whois # this is python-whois
import argparse import argparse
import sys import sys
from pathlib import Path from pathlib import Path
GOV_URLS_CSV_URL = "https://raw.githubusercontent.com/GSA/govt-urls/master/1_govt_urls_full.csv"
GOV_URLS_CSV_URL = (
"https://raw.githubusercontent.com/GSA/govt-urls/master/1_govt_urls_full.csv"
)
data = requests.get(GOV_URLS_CSV_URL).text data = requests.get(GOV_URLS_CSV_URL).text
csv_data = list(csv.reader(data.splitlines(), delimiter=',')) csv_data = list(csv.reader(data.splitlines(), delimiter=","))
domains = csv_data[1:] domains = csv_data[1:]
fields = csv_data[0] + ['Registrar'] fields = csv_data[0] + ["Registrar"]
def check_registration(name): def check_registration(name):
try: try:
domain_info = whois.whois(name) domain_info = whois.whois(name)
return domain_info['registrar'] return domain_info["registrar"]
except KeyboardInterrupt: except KeyboardInterrupt:
sys.exit(1) sys.exit(1)
except: except:
print(f'Something went wrong with that domain lookup for {name}, continuing...') print(f"Something went wrong with that domain lookup for {name}, continuing...")
def main(domain): def main(domain):
@ -40,7 +44,11 @@ def main(domain):
else: else:
for idx, domain in enumerate(domains): for idx, domain in enumerate(domains):
domain_name = domain[0].lower() domain_name = domain[0].lower()
if domain_name.endswith('.com') or domain_name.endswith('.edu') or domain_name.endswith('.net'): if (
domain_name.endswith(".com")
or domain_name.endswith(".edu")
or domain_name.endswith(".net")
):
print(idx) print(idx)
print(domain_name) print(domain_name)
registrar = check_registration(domain_name) registrar = check_registration(domain_name)
@ -48,15 +56,17 @@ def main(domain):
Path("../data").mkdir(exist_ok=True) Path("../data").mkdir(exist_ok=True)
with open('../data/registrar_data.csv', 'w') as f: with open("../data/registrar_data.csv", "w") as f:
writer = csv.writer(f) writer = csv.writer(f)
writer.writerow(fields) writer.writerow(fields)
writer.writerows(full_data) writer.writerows(full_data)
if __name__ == '__main__': if __name__ == "__main__":
cl = argparse.ArgumentParser(description="This performs ICANN lookups on domains.") cl = argparse.ArgumentParser(description="This performs ICANN lookups on domains.")
cl.add_argument("--domain", help="finds the registrar for a single domain", default=None) cl.add_argument(
"--domain", help="finds the registrar for a single domain", default=None
)
args = cl.parse_args() args = cl.parse_args()
sys.exit(main(args.domain)) sys.exit(main(args.domain))

View file

@ -10,12 +10,15 @@ This script can be run locally to generate data and currently takes some time to
import csv import csv
import requests import requests
DOMAIN_LIST_URL = "https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv" DOMAIN_LIST_URL = (
"https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv"
)
data = requests.get(DOMAIN_LIST_URL).content.decode("utf-8")
csv_data = list(csv.reader(data.splitlines(), delimiter=","))
domains = csv_data[1:]
fields = csv_data[0] + ["Response"]
data = requests.get(DOMAIN_LIST_URL).content.decode('utf-8')
csv_data = list(csv.reader(data.splitlines(), delimiter=','))
domains = csv_data[1:]
fields = csv_data[0] + ['Response']
def check_status_response(domain): def check_status_response(domain):
try: try:
@ -24,13 +27,14 @@ def check_status_response(domain):
response = type(e).__name__ response = type(e).__name__
return response return response
full_data = [] full_data = []
for domain in domains: for domain in domains:
domain_name = domain[0] domain_name = domain[0]
response = check_status_response(domain_name) response = check_status_response(domain_name)
full_data.append(domain + [response]) full_data.append(domain + [response])
with open('../data/response_codes.csv', 'w') as f: with open("../data/response_codes.csv", "w") as f:
writer = csv.writer(f) writer = csv.writer(f)
writer.writerow(fields) writer.writerow(fields)
writer.writerows(full_data) writer.writerows(full_data)

2
src/.bandit Normal file
View file

@ -0,0 +1,2 @@
[bandit]
exclude: docker_entrypoint.py

4
src/.flake8 Normal file
View file

@ -0,0 +1,4 @@
[flake8]
max-line-length = 88
max-complexity = 10
extend-ignore = E203

1
src/.python-version Symbolic link
View file

@ -0,0 +1 @@
src/runtime.txt

View file

@ -1,6 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
"""Django's command-line utility for administrative tasks.""" """Django's command-line utility for administrative tasks."""
import os
import sys import sys
@ -17,5 +16,5 @@ def main():
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

5
src/pyproject.toml Normal file
View file

@ -0,0 +1,5 @@
[tool.black]
line-length=88
[tool.mypy]
ignore_missing_imports = true

View file

@ -43,11 +43,8 @@ SECRET_KEY = secret("DJANGO_SECRET_KEY")
DEBUG = env.bool("DJANGO_DEBUG", default=False) DEBUG = env.bool("DJANGO_DEBUG", default=False)
# TODO: configure and document security settings # TODO: configure and document security settings
ALLOWED_HOSTS = [ ALLOWED_HOSTS = ["getgov-unstable.app.cloud.gov", "get.gov"]
'getgov-unstable.app.cloud.gov', ALLOWED_CIDR_NETS = ["10.0.0.0/8"] # nosec
'get.gov'
]
ALLOWED_CIDR_NETS = ['10.0.0.0/8'] # nosec
USE_X_FORWARDED_HOST = True USE_X_FORWARDED_HOST = True
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_HTTPONLY = True
@ -58,39 +55,39 @@ CORS_ALLOW_ALL_ORIGINS = False
# TODO: are all of these needed? Need others? # TODO: are all of these needed? Need others?
INSTALLED_APPS = [ INSTALLED_APPS = [
'django.contrib.admin', "django.contrib.admin",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.staticfiles', "django.contrib.staticfiles",
] ]
# TODO: document these for future maintainers # TODO: document these for future maintainers
MIDDLEWARE = [ MIDDLEWARE = [
'allow_cidr.middleware.AllowCIDRMiddleware', "allow_cidr.middleware.AllowCIDRMiddleware",
'django.middleware.security.SecurityMiddleware', "django.middleware.security.SecurityMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.common.CommonMiddleware', "django.middleware.common.CommonMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
'csp.middleware.CSPMiddleware', "csp.middleware.CSPMiddleware",
] ]
# TODO: decide on template engine and document in ADR # TODO: decide on template engine and document in ADR
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'DIRS': [BASE_DIR / 'templates'], "DIRS": [BASE_DIR / "templates"],
'APP_DIRS': True, "APP_DIRS": True,
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.template.context_processors.debug', "django.template.context_processors.debug",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
@ -104,7 +101,7 @@ LOGGING = {
"formatters": { "formatters": {
"verbose": { "verbose": {
"format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] " "format": "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] "
"%(message)s", "%(message)s", # noqa
"datefmt": "%d/%b/%Y %H:%M:%S", "datefmt": "%d/%b/%Y %H:%M:%S",
}, },
"simple": { "simple": {
@ -141,15 +138,15 @@ LOGGING = {
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases # https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = { DATABASES = {
'default': env.dj_db_url('DATABASE_URL'), "default": env.dj_db_url("DATABASE_URL"),
} }
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/ # https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC' TIME_ZONE = "UTC"
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = True
USE_L10N = True USE_L10N = True
@ -157,13 +154,13 @@ USE_L10N = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/ # https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_ROOT = BASE_DIR / 'static' STATIC_ROOT = BASE_DIR / "static"
STATIC_URL = 'static/' STATIC_URL = "static/"
ADMIN_URL = 'admin/' ADMIN_URL = "admin/"
ROOT_URLCONF = 'registrar.config.urls' ROOT_URLCONF = "registrar.config.urls"
WSGI_APPLICATION = 'registrar.config.wsgi.application' WSGI_APPLICATION = "registrar.config.wsgi.application"
# TODO: FAC example for REST framework # TODO: FAC example for REST framework
API_VERSION = "0" API_VERSION = "0"
@ -186,13 +183,13 @@ REST_FRAMEWORK = {
# TODO: FAC example for login.gov # TODO: FAC example for login.gov
SIMPLE_JWT = { SIMPLE_JWT = {
'ALGORITHM': 'RS256', "ALGORITHM": "RS256",
'AUDIENCE': None, "AUDIENCE": None,
'ISSUER': 'https://idp.int.identitysandbox.gov/', "ISSUER": "https://idp.int.identitysandbox.gov/",
'JWK_URL': 'https://idp.int.identitysandbox.gov/api/openid_connect/certs', "JWK_URL": "https://idp.int.identitysandbox.gov/api/openid_connect/certs",
'LEEWAY': 0, "LEEWAY": 0,
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.UntypedToken',), "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.UntypedToken",),
'USER_ID_CLAIM': 'sub', "USER_ID_CLAIM": "sub",
} }
TOKEN_AUTH = {"TOKEN_TTL": 3600} TOKEN_AUTH = {"TOKEN_TTL": 3600}
@ -200,4 +197,4 @@ TOKEN_AUTH = {"TOKEN_TTL": 3600}
# Default primary key field type # Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

View file

@ -10,14 +10,11 @@ from django.urls import include, path
from registrar.views import health from registrar.views import health
urlpatterns = [ urlpatterns = [path("admin/", admin.site.urls), path("health/", health.health)]
path("admin/", admin.site.urls),
path("health/", health.health)
]
if settings.DEBUG: if settings.DEBUG:
import debug_toolbar import debug_toolbar
urlpatterns = [ urlpatterns = [
path("__debug__/", include(debug_toolbar.urls)), path("__debug__/", include(debug_toolbar.urls)),
] + urlpatterns ] + urlpatterns

View file

@ -7,8 +7,6 @@ For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/
""" """
import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -1,4 +1,5 @@
from django.http import HttpResponse from django.http import HttpResponse
def health(request): def health(request):
return HttpResponse("OK") return HttpResponse("OK")