mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-05-14 17:24:57 +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_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_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_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_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
|
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