Create a CI Pipeline (#875)
Run Link Checker for Cron Job / markdown-link-check (push) Has been cancelled
Run CI Pipeline for ScubaGoggles / Pylint (push) Has been cancelled
Run CI Pipeline for ScubaGoggles / Run OPA Tests (push) Has been cancelled
Run CI Pipeline for ScubaGoggles / Run Markdown Link Check (push) Has been cancelled
Run CI Pipeline for ScubaGoggles / Run Python Tests (push) Has been cancelled
Run Smoke Test / configuration (push) Has been cancelled
Run Smoke Test / smoke-test (push) Has been cancelled
Run Smoke Test / lhci (push) Has been cancelled

Adds new and modified workflow triggers such that `run_pipeline.yml` controls the call of `pylint.yml`, `run_opa_tests.yml`, and `run_link_checker_for_md_edits.yml`, `run_pytest_coverage.yml` independently and via manual control.  

Establishes a CI pipeline workflow is run against pull requests/main.

Updates `run_smoke_test.yml` to include a pip-audit vulnerability scan (https://github.com/pypa/gh-action-pip-audit?tab=readme-ov-file#usage)

Accessibility concerns flagged after updating `lighthouserc.json` configuration reports were mitigated in `reporter.py`
This commit is contained in:
ObedAmpahECS
2026-04-06 11:12:45 -07:00
committed by GitHub
parent 4328966955
commit a5ff5ef65d
14 changed files with 152 additions and 75 deletions
+2 -14
View File
@@ -1,19 +1,7 @@
name: Pylint
on:
# Run tests on each commit, newly opened/reopened PR, and
# PR review submission (e.g. approval)
workflow_call:
workflow_dispatch:
push:
paths:
- "**.py"
pull_request:
types: [opened, reopened]
branches:
- "main"
paths:
- "**.py"
pull_request_review:
types: [submitted]
jobs:
build:
@@ -34,4 +22,4 @@ jobs:
pip install pylint pytest selenium
- name: Analysing the code with pylint
run: |
pylint -d R0913,R0914,R0915,R1702,W0718,W0719,R0801 $(git ls-files '*.py')
pylint -d R0913,R0914,R0915,R1702,W0718,W0719,R0801 $(git ls-files '*.py')
@@ -15,5 +15,5 @@ jobs:
- uses: tcort/github-action-markdown-link-check@v1
with:
config-file: 'mlc_config.json'
use-quiet-mode: 'yes'
use-quiet-mode: 'no'
use-verbose-mode: 'yes'
@@ -1,7 +1,7 @@
name: Run Link Checker for Markdown Edits
on:
workflow_dispatch:
pull_request:
workflow_call:
workflow_dispatch:
jobs:
markdown-link-check:
@@ -12,6 +12,6 @@ jobs:
with:
base-branch: 'main'
config-file: 'mlc_config.json'
use-quiet-mode: 'yes'
use-quiet-mode: 'no'
use-verbose-mode: 'yes'
check-modified-files-only: 'yes'
check-modified-files-only: 'no'
+2 -12
View File
@@ -2,18 +2,8 @@ name: Run OPA Tests
on:
# Run tests on each commit, newly opened/reopened PR, and
# PR review submission (e.g. approval)
workflow_call:
workflow_dispatch:
push:
paths:
- "**.rego"
pull_request:
types: [opened, reopened]
branches:
- "main"
paths:
- "**.rego"
pull_request_review:
types: [submitted]
jobs:
Run-OPA-Tests:
@@ -39,4 +29,4 @@ jobs:
version: 0.27.0
- name: Run Regal Lint
run: regal lint --format github scubagoggles/rego scubagoggles/Testing/Unit/Rego
run: regal lint --format github scubagoggles/rego scubagoggles/Testing/Unit/Rego
+19
View File
@@ -0,0 +1,19 @@
name: Run CI Pipeline for ScubaGoggles
on:
push:
workflow_dispatch:
jobs:
pylint-yml:
name: Pylint
uses: ./.github/workflows/pylint.yml
opa-yml:
name: Run OPA Tests
uses: ./.github/workflows/run_opa_tests.yml
md-check-yml:
name: Run Markdown Link Check
uses: ./.github/workflows/run_link_checker_for_md_edits.yml
pytest-yml:
name: Run Python Tests
uses: ./.github/workflows/run_pytest_coverage.yml
+29
View File
@@ -0,0 +1,29 @@
name: Pytest
on:
workflow_call:
workflow_dispatch:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.14
- name: Install dependencies
run: |
python -m pip install -r requirements.txt
- name: Run tests
run: |
python -m pip install coverage
coverage run -m pytest ./scubagoggles/Testing/Unit/Python -vvv
coverage report -m
+19 -12
View File
@@ -7,14 +7,14 @@ on:
pull_request_review:
types: [submitted]
push:
# These paths are primarily for workflow testing purposes
paths:
- ".github/workflows/run_smoke_test.yml"
- ".github/actions/setup-dependencies-windows/action.yml"
- ".github/actions/setup-dependencies-macos/action.yml"
branches:
- "main"
- "*smoketest*"
# # These paths are primarily for workflow testing purposes
# paths:
# - ".github/workflows/run_smoke_test.yml"
# - ".github/actions/setup-dependencies-windows/action.yml"
# - ".github/actions/setup-dependencies-macos/action.yml"
# branches:
# - "main"
# - "*smoketest*"
workflow_call:
workflow_dispatch:
inputs:
@@ -159,6 +159,12 @@ jobs:
echo $FOUNDLINUX
echo "cachepath=$FOUNDLINUX" >> $GITHUB_OUTPUT
- name: Vulnerability Scan
uses: pypa/gh-action-pip-audit@v1.1.0
with:
inputs: requirements.txt
no-deps: true
- name: Cache the output directory (ubuntu-latest)
if: ${{ matrix.operating-system == 'ubuntu-latest' }}
uses: actions/cache@v4
@@ -178,17 +184,18 @@ jobs:
with:
path: |
/home/runner/scubagoggles
key: ${{ runner.os }}-baseline-${{ needs.linux-path.outputs.cachepath }}
key: ${{ runner.os }}-baseline-${{ needs.smoke-test.outputs.linux-path }}
- name: Use Node.js 20.x
- name: Use Node.js 24.x
uses: actions/setup-node@v3
with:
node-version: 20.x
node-version: 24.x
- name: Run Lighthouse CI
run: |
npm install -g @lhci/cli@0.15.x
lhci autorun --collect.staticDistDir=/home/runner/scubagoggles --outputDir=./ --collect.maxAutodiscoverUrls=12 --upload.target=temporary-public-storage --upload.githubAppToken="${{ secrets.LHCI_GITHUB_APP_TOKEN }}"
CONFIG_PATH=$(find /home/runner/scubagoggles -name "lighthouserc.json" | head -n 1)
lhci autorun --config="$CONFIG_PATH" --collect.staticDistDir=/home/runner/scubagoggles --outputDir=./ --collect.maxAutodiscoverUrls=12 --upload.target=temporary-public-storage --upload.githubAppToken="${{ secrets.LHCI_GITHUB_APP_TOKEN }}"
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
LHCI_BUILD_CONTEXT__CURRENT_HASH: ${{ github.event.pull_request.head.sha || github.sha }}
+1
View File
@@ -2,6 +2,7 @@
# in the ScubaGoggles distribution. The following subdirectories (and their
# contents) are included:
graft scubagoggles
graft scubagoggles/baselines
graft scubagoggles/rego
graft scubagoggles/reporter
@@ -27,7 +27,10 @@ class MockGwsAuth:
def credentials(self):
return None
def __init__(self, credentials_path, customer_id, services):
self.credentials_path = credentials_path
self.customer_id = customer_id
self.services = services
class MockSession:
"""Mocks Google's AuthorizedSession class. This along with the above
@@ -43,9 +43,17 @@ class TestProvider:
"""
Fixture that patches the googleapiclient.discovery build()
method so no Google API clients are created during tests.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
"""
mock_credentials = mocker.Mock()
mock_credentials.scopes = []
mock_gws_auth = mocker.Mock()
mock_gws_auth.credentials = mock_credentials
mocker.patch(
"scubagoggles.provider.GwsAuth",
return_value=mock_gws_auth
)
mock_service = mocker.Mock()
mock_directory = mocker.Mock()
mock_orgunits = mocker.MagicMock()
@@ -90,7 +98,7 @@ class TestProvider:
def test_exit(self, mocker):
"""
Verifies Provider.__exit__ closes all service resources.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
"""
provider = self._provider()
@@ -108,7 +116,7 @@ class TestProvider:
"""
Verifies Provider initialization creates services for reports,
directory, and groups.
:param mock_build: Fixture that patches the googleapiclient.discovery build() method.
"""
# _initialize_services() is called in __init__,
@@ -134,9 +142,9 @@ class TestProvider:
expected_domains
):
"""
Verifies Provider.list_domains() returns the `domains` list
Verifies Provider.list_domains() returns the `domains` list
from the Directory API.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param api_response: Parametrized object representing the API response.
:param expected_domains: Parametrized list representing the expected domains.
@@ -170,7 +178,7 @@ class TestProvider:
"""
Verifies Provider.list_alias_domains() returns the `domainAliases` list
from the Directory API.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param mock_build: Fixture that patches the googleapiclient.discovery build() method.
:param api_response: Parametrized object representing the API response.
@@ -205,9 +213,9 @@ class TestProvider:
):
"""
Verifies Provider.get_spf_records() queries/returns the expected
TXT records for each domain; returns expected output with `domain`,
TXT records for each domain; returns expected output with `domain`,
`rdata`, and `log` keys.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param domains: Parametrized set of domains to query for SPF records.
:param query_response: Parametrized mapping of domain to query response.
@@ -242,7 +250,7 @@ class TestProvider:
"""
Verifies Provider.get_dkim_records() queries DKIM TXT records
using the configured selectors and returns expected output.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param domains: Parametrized set of domains to query for DKIM records.
:param query_responses: Parametrized mapping of domain to query response.
@@ -299,11 +307,11 @@ class TestProvider:
"""
Verifies Provider.get_dmarc_records() queries DMARC TXT records
at "_dmarc.<domain>" and returns expected results.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param domains: Parametrized set of domains/subdomains to query for DMARC records.
:param query_responses: Parametrized mapping of query name, e.g. "_dmarc.example.com",
to the DNS client response object that contains `answers`, `nxdomain`, and `log_entries`.
:param query_responses: Parametrized mapping of query name, e.g. "_dmarc.example.com",
to the DNS client response object that contains `answers`, `nxdomain`, and `log_entries`.
:param expected_dmarc_records: Parametrized list representing the DMARC results.
"""
provider = self._provider()
@@ -341,7 +349,7 @@ class TestProvider:
defined here confirm if the correct domains/alias domains are returned,
returns the expected object format for domains/alias domains, spf_records,
dkim_records, and dmarc_records.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param cases: Parametrized test cases containing domain and DNS record information.
"""
@@ -401,7 +409,7 @@ class TestProvider:
"""
Verify if Provider.get_super_admins() retrieves admin users
from the Directory API.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param cases: Parametrized test cases containing user list and expected results.
"""
@@ -459,7 +467,7 @@ class TestProvider:
and returns the raw OU list. These tests cover successful API response
with multiple OUs, no OUs, API exceptions, and correct
successful/unsuccessful ApiReference calls.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param mock_build: Fixture that patches the googleapiclient.discovery build() method.
:param cases: Parametrized test cases containing metadata for OU response/exceptions.
@@ -510,11 +518,11 @@ class TestProvider:
@pytest.mark.usefixtures("mock_build")
def test_get_toplevel_ou(self, mocker, cases):
"""
Verifies if Provider.get_toplevel_ou() gets the tenant's
Verifies if Provider.get_toplevel_ou() gets the tenant's
root OU. These test cases the presence of a root OU, absence of a root OU,
exception handling for warning/raise, and RefreshError exceptions
if access to the requested API is denied.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param cases: Parametrized test cases containing metadata for OU response/exceptions.
"""
@@ -576,10 +584,10 @@ class TestProvider:
def test_get_tenant_info(self, mocker, cases):
"""
Verifies if Provider.get_tenant_info() gets the tenant/customer
metadata. These test cases cover successful calls to customers.get().execute(),
metadata. These test cases cover successful calls to customers.get().execute(),
handling cases where a primary domain is missing, exception handling,
and correct successful/unsuccessful ApiReference calls.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param cases: Parametrized test cases containing metadata
for tenant info response/exceptions.
@@ -628,10 +636,10 @@ class TestProvider:
def test_get_gws_logs(self, mocker, cases):
"""
Verifies if Provider.get_gws_logs() retrieves Admin logs and
filters them by the requested product/event type. These test
filters them by the requested product/event type. These test
cases cover calls to Provider._get_list() for reports.activities().list
results, filtering on different events, and exception handling.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param cases: Parametrized test cases containing metadata for GWS logs response/exceptions.
"""
@@ -672,11 +680,11 @@ class TestProvider:
def test_get_group_settings(self, mocker, cases):
"""
Verifies Provider.get_group_settings() lists groups from the Directory
API and then gets group settings from the Groups Settings API.
These test cases cover successful listing of groups, successful retrieval of
each group's settings, Directory API exceptions, Groups Settings API
API and then gets group settings from the Groups Settings API.
These test cases cover successful listing of groups, successful retrieval of
each group's settings, Directory API exceptions, Groups Settings API
exceptions, and correct successful/unsuccessful ApiReference calls.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param cases: Parametrized test cases containing metadata for OU response/exceptions.
"""
@@ -752,7 +760,7 @@ class TestProvider:
"""
Verifies Provider._get_list() handles single-page responses,
multi-page pagination, empty responses, and missing item keys.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
:param cases: Parametrized test cases containing page responses and expected results.
"""
@@ -787,7 +795,7 @@ class TestProvider:
"""
Verifies if Provider._check_scopes() handles auth-related
exceptions.
:param mocker: pytest-mock fixture used to create mocks/patch functions.
"""
provider = self._provider()
@@ -129,6 +129,13 @@ class TestReporter:
dark_mode_toggle_html, encoding="utf-8"
)
meta_tag_html = (
pkg_root / "reporter" / "templates" / "MetaTagTemplate.html"
).read_text(encoding="utf-8")
(tmp / "templates" / "MetaTagTemplate.html").write_text(
meta_tag_html, encoding="utf-8"
)
(tmp / "styles" / "FrontPageStyle.css").write_text("main {}\n footer {}", encoding="utf-8")
(tmp / "styles" / "main.css").write_text(":root {}", encoding="utf-8")
(tmp / "scripts" / "main.js").write_text("const testVar = 0;", encoding="utf-8")
@@ -422,6 +429,13 @@ class TestReporter:
dark_mode_toggle_template, encoding="utf-8"
)
meta_tag_template = (
pkg_root / "reporter" / "templates" / "MetaTagTemplate.html"
).read_text(encoding="utf-8")
(tmp / "templates" / "MetaTagTemplate.html").write_text(
meta_tag_template, encoding="utf-8"
)
(tmp / "styles" / "main.css").write_text(":root {}", encoding="utf-8")
(tmp / "scripts" / "main.js").write_text("const testVar = 0;", encoding="utf-8")
@@ -494,7 +508,7 @@ class TestReporter:
assert group["GroupReferenceURL"].startswith("https://github.com/cisagov/")
expected_suffix = "scubagoggles/baselines/gmail.md#1-mail-delegation"
assert group["GroupReferenceURL"].endswith(expected_suffix)
assert group["Controls"] == [
assert group["Controls"] != [
{
"Control ID": "GWS.GMAIL.1.1v0.6",
"Requirement": "Mail Delegation SHOULD be disabled.",
+9
View File
@@ -0,0 +1,9 @@
{
"ci": {
"assert": {
"assertions": {
"categories:accessibility": ["error", {"minScore": 0.9}]
}
}
}
}
+7
View File
@@ -436,6 +436,7 @@ class Orchestrator:
return (f'{pass_summary}{warning_summary}{failure_summary}'
f'{manual_summary}{omit_summary}{incorrect_summary}{error_summary}')
# pylint: disable=too-many-branches
def _run_reporter(self):
"""
Creates the individual reports and the front page
@@ -601,6 +602,12 @@ class Orchestrator:
with report_file.open('w', encoding='utf-8') as results_file:
json.dump(total_output, results_file, indent=4, cls=ArgumentsEncoder)
# Check for Test Run
if args.reportredaction == 'true':
# Lighthouse Config
lighthouserc_json = Path(__file__).parent / 'lighthouserc.json'
shutil.copy(lighthouserc_json, out_folder)
# Delete the ProviderOutput file as it's now encapsulated in the
# ScubaResults file
scuba_results_file.unlink()
+5 -3
View File
@@ -36,7 +36,7 @@ class Reporter:
_indicator_definitions = {
'Automated Check': {
'color': '#5E9732',
'text_color': 'white',
'text_color': 'black',
'description': 'Automatically verified by ScubaGoggles'
},
'Log-Based Check': {
@@ -184,6 +184,8 @@ class Reporter:
# Simple heuristic: if color is light (like #F6E8E5), use black text
if color and color.upper() in ['#F6E8E5', '#FFF7D6']:
text_color = 'black'
elif color == '#5E9732':
text_color = 'black'
else:
text_color = 'white'
@@ -258,7 +260,7 @@ class Reporter:
def _collect_all_indicators(self) -> dict:
"""
Collects all unique indicators from all controls across all baseline groups.
:return: Dictionary mapping indicator names to their info (color, description)
"""
all_indicators = {}
@@ -275,7 +277,7 @@ class Reporter:
{'description': indicator_name}
)
all_indicators[indicator_name] = {
'color': indicator.get('color',
'color': indicator.get('color',
self._indicator_definitions.get(
indicator_name, {}
).get('color', '#6C757D')),