diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 09a858a434db..b7c5b0d507f9 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -992,6 +992,7 @@ ./services/monitoring/gitwatch.nix ./services/monitoring/glances.nix ./services/monitoring/glpi-agent.nix + ./services/monitoring/go-csp-collector.nix ./services/monitoring/goss.nix ./services/monitoring/grafana-image-renderer.nix ./services/monitoring/grafana-reporter.nix diff --git a/nixos/modules/services/monitoring/go-csp-collector.nix b/nixos/modules/services/monitoring/go-csp-collector.nix new file mode 100644 index 000000000000..6fafbb840740 --- /dev/null +++ b/nixos/modules/services/monitoring/go-csp-collector.nix @@ -0,0 +1,106 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.go-csp-collector; + + inherit (lib) + boolToString + concatStringsSep + getExe + isBool + literalExpression + maintainers + mapAttrsToList + mkEnableOption + mkIf + mkOption + mkPackageOption + types + ; + + settingsToArgs = + settings: + concatStringsSep " " ( + mapAttrsToList ( + name: value: + let + flag = "-${name}"; + in + if isBool value then "${flag}=${boolToString value}" else "${flag} ${toString value}" + ) settings + ); +in +{ + meta.maintainers = with maintainers; [ stepbrobd ]; + + options.services.go-csp-collector = { + enable = mkEnableOption "go-csp-collector, a content security policy violation collector"; + + package = mkPackageOption pkgs "go-csp-collector" { }; + + settings = mkOption { + type = types.submodule { + freeformType = + with types; + attrsOf (oneOf [ + bool + path + str + ]); + + options = { + port = mkOption { + type = types.port; + description = "The port to listen on."; + default = 8080; + example = 8080; + }; + + output-format = mkOption { + type = types.enum [ + "text" + "json" + ]; + description = "Define how the violation reports are formatted for output."; + default = "text"; + example = "text"; + }; + }; + }; + + description = '' + Settings for go-csp-collector. See + for supported options. + ''; + + default = { }; + + example = literalExpression '' + { + debug = true; + health-check-path = "/health"; + } + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.packages = [ cfg.package ]; + systemd.services.go-csp-collector = { + description = "CSP violation collector"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + ReadOnlyPaths = cfg.settings.filter-file or ""; + ExecStart = [ + "" + "${getExe cfg.package} ${settingsToArgs cfg.settings}" + ]; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index bcb00f3b067f..511e2101fa7b 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -646,6 +646,7 @@ in gns3-server = runTest ./gns3-server.nix; gnupg = runTest ./gnupg.nix; go-camo = runTest ./go-camo.nix; + go-csp-collector = runTest ./go-csp-collector.nix; go-httpbin = runTest ./go-httpbin.nix; go-neb = runTest ./go-neb.nix; goatcounter = runTest ./goatcounter.nix; diff --git a/nixos/tests/go-csp-collector.nix b/nixos/tests/go-csp-collector.nix new file mode 100644 index 000000000000..ec75b51506ff --- /dev/null +++ b/nixos/tests/go-csp-collector.nix @@ -0,0 +1,66 @@ +{ lib, ... }: + +{ + name = "go-csp-collector"; + meta.maintainers = with lib.maintainers; [ stepbrobd ]; + + nodes.machine = + { pkgs, ... }: + { + services.go-csp-collector = { + enable = true; + settings = { + debug = true; + port = 9999; + health-check-path = "/health"; + filter-file = pkgs.writeText "filter" "chrome-extension://"; + }; + }; + }; + + testScript = '' + import json + + # health check + machine.wait_for_unit("go-csp-collector.service") + machine.wait_for_open_port(9999) + machine.succeed("curl -f http://localhost:9999/health") + + # send valid csp report + machine.succeed( + "curl -f -X POST http://127.0.0.1:9999/ " + "-H 'Content-Type: application/csp-report' " + "-d '" + json.dumps({ + "csp-report": { + "document-uri": "https://example.com/", + "referrer": "https://example.com/", + "violated-directive": "script-src", + "effective-directive": "script-src", + "original-policy": "script-src 'self'", + "blocked-uri": "https://example.org/malicious.js", + "status-code": 200 + } + }) + "'" + ) + logs = machine.succeed("journalctl -u go-csp-collector.service") + assert "level=debug" in logs, "debug mode not enabled" + assert "blocked_uri" in logs, "csp report not logged" + assert "https://example.org/malicious.js" in logs, "blocked uri not in logs" + + # check rejection + machine.fail( + "curl -f -X POST http://[::1]:9999/ " + "-H 'Content-Type: application/csp-report' " + "-d '" + json.dumps({ + "csp-report": { + "document-uri": "https://example.com/", + "blocked-uri": "chrome-extension://something", + "violated-directive": "script-src" + } + }) + "'" + ) + logs = machine.succeed("journalctl -u go-csp-collector.service") + assert "invalid resource" in logs, "filter rejection not logged" + assert "chrome-extension://" in logs, "filtered uri pattern not in logs" + ''; +} diff --git a/pkgs/by-name/go/go-csp-collector/package.nix b/pkgs/by-name/go/go-csp-collector/package.nix new file mode 100644 index 000000000000..c8437b2b122b --- /dev/null +++ b/pkgs/by-name/go/go-csp-collector/package.nix @@ -0,0 +1,53 @@ +{ + lib, + buildGoModule, + fetchFromGitHub, + versionCheckHook, + nix-update-script, + nixosTests, +}: + +buildGoModule (finalAttrs: { + pname = "go-csp-collector"; + version = "0.0.16-unstable-2025-10-10"; + + src = fetchFromGitHub { + owner = "jacobbednarz"; + repo = "go-csp-collector"; + rev = "a0cf22ac6d1f5c8972bf53671ba174767d2adcd5"; + hash = "sha256-xFvO8ZuJQ5luCDOTPHtVeb1+3VvIKSwjt2TqkxBIY58="; + }; + + vendorHash = "sha256-SrQahSHO5ZIkcLR3BR5CR5BTStW1pH1Ij1Eql0b3tuU="; + + ldflags = [ + "-s" + "-w" + "-X main.Rev=${finalAttrs.version}" + ]; + + postInstall = '' + install -Dm644 init/go-csp-collector.service $out/lib/systemd/system/go-csp-collector.service + + substituteInPlace $out/lib/systemd/system/go-csp-collector.service \ + --replace-fail "/usr/local/bin/go-csp-collector" "$out/bin/go-csp-collector" + ''; + + doInstallCheck = true; + nativeInstallCheckInputs = [ versionCheckHook ]; + versionCheckProgramArg = "-version"; + + passthru = { + updateScript = nix-update-script { }; + tests.service = nixosTests.go-csp-collector; + }; + + meta = { + description = "A content security policy violation collector written in Golang"; + homepage = "https://github.com/jacobbednarz/go-csp-collector"; + changelog = "https://github.com/jacobbednarz/go-csp-collector/blob/${finalAttrs.src.rev}/CHANGELOG.md"; + license = lib.licenses.mit; + mainProgram = "go-csp-collector"; + maintainers = with lib.maintainers; [ stepbrobd ]; + }; +})