mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-05-14 09:14:56 +02:00
Added draft for CSP health checker
This commit is contained in:
parent
72baa9ea28
commit
23496f2fab
@ -5,6 +5,7 @@ on_calendar_health_journalctl: "*-*-* 00:00:00"
|
||||
on_calendar_health_disc_space: "*-*-* 06,12,18,00:00:00" # Check four times per day if there is sufficient disc space
|
||||
on_calendar_health_docker_container: "*-*-* {{ hours_server_awake }}:00:00" # Check once per hour if the docker containers are healthy
|
||||
on_calendar_health_docker_volumes: "*-*-* {{ hours_server_awake }}:15:00" # Check once per hour if the docker volumes are healthy
|
||||
on_calendar_health_csp_crawler: "*-*-* {{ hours_server_awake }}:30:00" # Check once per hour if all CSP are fullfilled available
|
||||
on_calendar_health_nginx: "*-*-* {{ hours_server_awake }}:45:00" # Check once per hour if all webservices are available
|
||||
on_calendar_health_msmtp: "*-*-* 00:00:00" # Check once per day SMTP Server
|
||||
|
||||
|
45
roles/health-csp/README.md
Normal file
45
roles/health-csp/README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Health CSP Crawler
|
||||
|
||||
## Description
|
||||
|
||||
This Ansible role automates the validation of [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) enforcement for all configured domains by crawling them using a Puppeteer-based Node.js script.
|
||||
|
||||
## Overview
|
||||
|
||||
Designed for Archlinux systems, this role periodically checks whether web resources (JavaScript, fonts, images, etc.) are blocked by CSP headers. It integrates Python and Node.js tooling and installs a systemd service with timer support.
|
||||
|
||||
## Features
|
||||
|
||||
- **CSP Resource Validation:** Uses Puppeteer to simulate browser requests and detect blocked resources.
|
||||
- **Domain Extraction:** Parses all `.conf` files in the NGINX config folder to determine the list of domains to check.
|
||||
- **Automated Execution:** Registers a systemd service and timer for recurring health checks.
|
||||
- **Error Notification:** Integrates with `systemd-notifier` for alerting on failure.
|
||||
|
||||
## Dependencies
|
||||
|
||||
This role depends on the following:
|
||||
|
||||
- [`nodejs`](../nodejs/)
|
||||
- [`npm`](../npm/)
|
||||
- [`systemd-notifier`](../systemd-notifier/)
|
||||
- [`systemd-timer`](../systemd-timer/)
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the following variables to customize behavior:
|
||||
|
||||
```yaml
|
||||
health_csp_crawler_folder: "{{ path_administrator_scripts }}health-csp/"
|
||||
on_calendar_health_csp_crawler: "daily"
|
||||
````
|
||||
|
||||
## License
|
||||
|
||||
CyMaIS NonCommercial License (CNCL)
|
||||
[https://s.veen.world/cncl](https://s.veen.world/cncl)
|
||||
|
||||
## Author
|
||||
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
[https://www.veen.world](https://www.veen.world)
|
0
roles/health-csp/files/__init__.py
Normal file
0
roles/health-csp/files/__init__.py
Normal file
43
roles/health-csp/files/health-csp.js
Normal file
43
roles/health-csp/files/health-csp.js
Normal file
@ -0,0 +1,43 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
const domains = process.argv.slice(2);
|
||||
|
||||
(async () => {
|
||||
let errorCounter = 0;
|
||||
const browser = await puppeteer.launch({ headless: 'new' });
|
||||
|
||||
for (const domain of domains) {
|
||||
const page = await browser.newPage();
|
||||
const blockedResources = [];
|
||||
|
||||
page.on('requestfailed', request => {
|
||||
const reason = request.failure()?.errorText || '';
|
||||
if (reason.includes('blocked')) {
|
||||
blockedResources.push({ url: request.url(), reason });
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const url = `https://${domain}`;
|
||||
await page.goto(url, { waitUntil: 'networkidle2', timeout: 20000 });
|
||||
} catch (e) {
|
||||
console.error(`${domain}: ERROR visiting site - ${e.message}`);
|
||||
errorCounter++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blockedResources.length > 0) {
|
||||
console.warn(`${domain}: Blocked resources detected:`);
|
||||
blockedResources.forEach(r =>
|
||||
console.log(` BLOCKED by CSP: ${r.url} (${r.reason})`)
|
||||
);
|
||||
errorCounter++;
|
||||
} else {
|
||||
console.log(`${domain}: ✅ No CSP blocks detected.`);
|
||||
}
|
||||
|
||||
await page.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
process.exit(errorCounter);
|
||||
})();
|
66
roles/health-csp/files/health_csp.py
Normal file
66
roles/health-csp/files/health_csp.py
Normal file
@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
|
||||
def extract_domains(config_path):
|
||||
"""
|
||||
Extracts domain names from .conf filenames in the given directory.
|
||||
"""
|
||||
domain_pattern = re.compile(r'^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}\.conf$')
|
||||
domains = []
|
||||
|
||||
try:
|
||||
for filename in os.listdir(config_path):
|
||||
if filename.endswith(".conf") and domain_pattern.match(filename):
|
||||
domain = filename[:-5] # Remove ".conf"
|
||||
domains.append(domain)
|
||||
except FileNotFoundError:
|
||||
print(f"Directory {config_path} not found.", file=sys.stderr)
|
||||
return None
|
||||
|
||||
return domains
|
||||
|
||||
|
||||
def run_node_checker(script_path, domains):
|
||||
"""
|
||||
Executes the Node.js CSP checker script with the given domains.
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["/usr/bin/node", script_path] + domains,
|
||||
check=True
|
||||
)
|
||||
return result.returncode
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"{os.path.basename(script_path)} reported issues (exit code {e.returncode})")
|
||||
return e.returncode
|
||||
except Exception as e:
|
||||
print(f"Unexpected error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Check CSP-blocked resources via Puppeteer")
|
||||
parser.add_argument("--nginx-config-dir", required=True, help="Directory containing NGINX .conf files")
|
||||
parser.add_argument("--script", required=True, help="Path to Node.js CSP checker script")
|
||||
args = parser.parse_args()
|
||||
|
||||
domains = extract_domains(args.nginx_config_dir)
|
||||
|
||||
if domains is None:
|
||||
return 1
|
||||
|
||||
if not domains:
|
||||
print("No domains found to check.")
|
||||
return 0
|
||||
|
||||
return run_node_checker(args.script, domains)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
5
roles/health-csp/handlers/main.yml
Normal file
5
roles/health-csp/handlers/main.yml
Normal file
@ -0,0 +1,5 @@
|
||||
- name: "reload health-csp.cymais.service"
|
||||
systemd:
|
||||
name: health-csp.cymais.service
|
||||
enabled: yes
|
||||
daemon_reload: yes
|
31
roles/health-csp/meta/main.yml
Normal file
31
roles/health-csp/meta/main.yml
Normal file
@ -0,0 +1,31 @@
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Checks for CSP-blocked resources via Puppeteer-based Node.js crawler"
|
||||
license: "CyMaIS NonCommercial License (CNCL)"
|
||||
license_url: "https://s.veen.world/cncl"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Archlinux
|
||||
versions:
|
||||
- rolling
|
||||
galaxy_tags:
|
||||
- csp
|
||||
- puppeteer
|
||||
- health
|
||||
- browser
|
||||
- nodejs
|
||||
- monitoring
|
||||
- systemd
|
||||
repository: "https://s.veen.world/cymais"
|
||||
issue_tracker_url: "https://s.veen.world/cymaisissues"
|
||||
documentation: "https://s.veen.world/cymais"
|
||||
dependencies:
|
||||
- systemd-notifier
|
||||
- nodejs
|
||||
- role: npm
|
||||
vars:
|
||||
npm_project_folder: "{{ health_csp_crawler_folder }}"
|
46
roles/health-csp/tasks/main.yml
Normal file
46
roles/health-csp/tasks/main.yml
Normal file
@ -0,0 +1,46 @@
|
||||
- name: "Install puppeteer if node_modules not yet present"
|
||||
ansible.builtin.command:
|
||||
cmd: npm install puppeteer
|
||||
chdir: "{{ health_csp_crawler_folder }}"
|
||||
creates: "{{ health_csp_crawler_folder }}/node_modules"
|
||||
|
||||
- name: Check if puppeteer is usable
|
||||
command: node -e "require('puppeteer')"
|
||||
args:
|
||||
chdir: "{{ health_csp_crawler_folder }}"
|
||||
register: puppeteer_check
|
||||
failed_when: puppeteer_check.rc != 0
|
||||
|
||||
- name: "create {{ health_csp_crawler_folder }}"
|
||||
file:
|
||||
path: "{{ health_csp_crawler_folder }}"
|
||||
state: directory
|
||||
mode: 0755
|
||||
|
||||
- name: copy health_csp.py
|
||||
copy:
|
||||
src: health_csp.py
|
||||
dest: "{{ health_csp_crawler_script }}"
|
||||
mode: 0755
|
||||
|
||||
- name: copy health-csp.js
|
||||
copy:
|
||||
src: health-csp.js
|
||||
dest: "{{ health_csp_crawler_node }}"
|
||||
mode: 0755
|
||||
|
||||
- name: create health-csp.cymais.service
|
||||
template:
|
||||
src: health-csp.service.j2
|
||||
dest: /etc/systemd/system/health-csp.cymais.service
|
||||
notify: reload health-csp.cymais.service
|
||||
|
||||
- name: set service_name to role_name
|
||||
set_fact:
|
||||
service_name: "{{ role_name }}"
|
||||
|
||||
- name: include systemd timer role
|
||||
include_role:
|
||||
name: systemd-timer
|
||||
vars:
|
||||
on_calendar: "{{ on_calendar_health_csp_crawler }}"
|
9
roles/health-csp/templates/health-csp.service.j2
Normal file
9
roles/health-csp/templates/health-csp.service.j2
Normal file
@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Check for CSP-blocked resources via Puppeteer
|
||||
OnFailure=systemd-notifier.cymais@%n.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/python3 {{ health_csp_crawler_script }} \
|
||||
--nginx-config-dir={{ nginx.directories.http.servers }} \
|
||||
--script={{ health_csp_crawler_node }}
|
3
roles/health-csp/vars/main.yml
Normal file
3
roles/health-csp/vars/main.yml
Normal file
@ -0,0 +1,3 @@
|
||||
health_csp_crawler_folder: "{{ path_administrator_scripts }}health-csp/"
|
||||
health_csp_crawler_script: "{{ health_csp_crawler_folder }}health_csp.py"
|
||||
health_csp_crawler_node: "{{ health_csp_crawler_folder }}health-csp.js"
|
35
roles/nodejs/README.md
Normal file
35
roles/nodejs/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# Node.js
|
||||
|
||||
## Description
|
||||
|
||||
This Ansible role installs Node.js on the target system using the native package manager.
|
||||
|
||||
## Overview
|
||||
|
||||
Optimized for Archlinux and Debian-based systems, this role ensures the presence of Node.js for use in Node-based applications or scripts. It serves as a foundational role for projects that depend on Node.js runtimes or utilities like Puppeteer.
|
||||
|
||||
## Features
|
||||
|
||||
- **Node.js Installation:** Installs the latest Node.js version available via the system's package manager.
|
||||
- **Idempotent Execution:** Ensures Node.js is only installed when missing.
|
||||
|
||||
## Usage
|
||||
|
||||
Include this role before running any tasks or roles that depend on Node.js:
|
||||
|
||||
```yaml
|
||||
- name: Ensure Node.js is available
|
||||
roles:
|
||||
- nodejs
|
||||
````
|
||||
|
||||
## License
|
||||
|
||||
CyMaIS NonCommercial License (CNCL)
|
||||
[https://s.veen.world/cncl](https://s.veen.world/cncl)
|
||||
|
||||
## Author
|
||||
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
[https://www.veen.world](https://www.veen.world)
|
24
roles/nodejs/meta/main.yml
Normal file
24
roles/nodejs/meta/main.yml
Normal file
@ -0,0 +1,24 @@
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Installs Node.js"
|
||||
license: "CyMaIS NonCommercial License (CNCL)"
|
||||
license_url: "https://s.veen.world/cncl"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Archlinux
|
||||
versions: [all]
|
||||
- name: Debian
|
||||
versions: [all]
|
||||
galaxy_tags:
|
||||
- nodejs
|
||||
- javascript
|
||||
- runtime
|
||||
- automation
|
||||
repository: "https://s.veen.world/cymais"
|
||||
issue_tracker_url: "https://s.veen.world/cymaisissues"
|
||||
documentation: "https://s.veen.world/cymais"
|
||||
dependencies: []
|
4
roles/nodejs/tasks/main.yml
Normal file
4
roles/nodejs/tasks/main.yml
Normal file
@ -0,0 +1,4 @@
|
||||
- name: Ensure Node.js is installed
|
||||
package:
|
||||
name: nodejs
|
||||
state: present
|
35
roles/npm/README.md
Normal file
35
roles/npm/README.md
Normal file
@ -0,0 +1,35 @@
|
||||
# npm
|
||||
|
||||
## Description
|
||||
|
||||
This Ansible role installs npm and optionally runs `npm ci` within a given project directory. It is intended to streamline dependency installation for Node.js applications.
|
||||
|
||||
## Overview
|
||||
|
||||
Designed for use in Node-based projects, this role installs npm and can execute a clean install (`npm ci`) to ensure consistent dependency trees.
|
||||
|
||||
## Features
|
||||
|
||||
- **npm Installation:** Ensures the `npm` package manager is installed.
|
||||
- **Optional Project Setup:** Runs `npm ci` in a specified folder to install exact versions from `package-lock.json`.
|
||||
- **Idempotent:** Skips `npm ci` if no folder is configured.
|
||||
|
||||
## Configuration
|
||||
|
||||
Set `npm_project_folder` to a directory containing `package.json` and `package-lock.json`:
|
||||
|
||||
```yaml
|
||||
vars:
|
||||
npm_project_folder: /opt/scripts/my-node-project/
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
CyMaIS NonCommercial License (CNCL)
|
||||
[https://s.veen.world/cncl](https://s.veen.world/cncl)
|
||||
|
||||
## Author
|
||||
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
[https://www.veen.world](https://www.veen.world)
|
24
roles/npm/meta/main.yml
Normal file
24
roles/npm/meta/main.yml
Normal file
@ -0,0 +1,24 @@
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Installs npm and runs optional 'npm ci' inside a project"
|
||||
license: "CyMaIS NonCommercial License (CNCL)"
|
||||
license_url: "https://s.veen.world/cncl"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
min_ansible_version: "2.9"
|
||||
platforms:
|
||||
- name: Archlinux
|
||||
versions: [all]
|
||||
- name: Debian
|
||||
versions: [all]
|
||||
galaxy_tags:
|
||||
- npm
|
||||
- nodejs
|
||||
- automation
|
||||
- javascript
|
||||
repository: "https://s.veen.world/cymais"
|
||||
issue_tracker_url: "https://s.veen.world/cymaisissues"
|
||||
documentation: "https://s.veen.world/cymais"
|
||||
dependencies: []
|
12
roles/npm/tasks/main.yml
Normal file
12
roles/npm/tasks/main.yml
Normal file
@ -0,0 +1,12 @@
|
||||
- name: Ensure npm is installed
|
||||
package:
|
||||
name: npm
|
||||
state: present
|
||||
|
||||
- name: Run 'npm ci' in {{ npm_project_folder }}
|
||||
command: npm ci
|
||||
args:
|
||||
chdir: "{{ npm_project_folder }}"
|
||||
when: npm_project_folder is defined
|
||||
register: npm_output
|
||||
changed_when: "'added' in npm_output.stdout or 'updated' in npm_output.stdout"
|
2
roles/npm/vars/main.yml
Normal file
2
roles/npm/vars/main.yml
Normal file
@ -0,0 +1,2 @@
|
||||
# Optional project folder to run 'npm ci' in
|
||||
npm_project_folder: null
|
39
tests/unit/test_health_csp_py.py
Normal file
39
tests/unit/test_health_csp_py.py
Normal file
@ -0,0 +1,39 @@
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 🧩 Add the path to the script under test
|
||||
SCRIPT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../roles/health-csp/files"))
|
||||
sys.path.insert(0, SCRIPT_PATH)
|
||||
|
||||
import health_csp
|
||||
|
||||
class TestHealthCspScript(unittest.TestCase):
|
||||
|
||||
@patch("os.listdir")
|
||||
def test_extract_domains_valid_files(self, mock_listdir):
|
||||
mock_listdir.return_value = ["example.com.conf", "sub.example.com.conf", "invalid.conf~"]
|
||||
domains = health_csp.extract_domains("/dummy/path")
|
||||
self.assertEqual(domains, ["example.com", "sub.example.com"])
|
||||
|
||||
@patch("os.listdir", side_effect=FileNotFoundError)
|
||||
def test_extract_domains_missing_dir(self, _):
|
||||
result = health_csp.extract_domains("/invalid/path")
|
||||
self.assertIsNone(result)
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_run_node_checker_success(self, mock_run):
|
||||
mock_run.return_value.returncode = 0
|
||||
code = health_csp.run_node_checker("/some/script.js", ["example.com"])
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
@patch("subprocess.run", side_effect=subprocess.CalledProcessError(3, "node"))
|
||||
def test_run_node_checker_failure(self, _):
|
||||
code = health_csp.run_node_checker("/some/script.js", ["fail.com"])
|
||||
self.assertEqual(code, 3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user