- vendor modules/anssi/ depuis SécurixOS (DINUM), copy-paste explicitement supporté par son propre README, licence MIT - hosts/common/base.nix: security.anssi.enable, level=intermediary, category=client -- remplace le bloc sysctl qui n'existait que sur le profil oreo (nixos_users), même valeurs, rien de plus fort - garde systemd.coredump.enable=false + apparmor (pas couvert par le module ANSSI, exception R45 explicite côté Sécurix) - ip_forward=0 (R12) n'affecte pas pxe-server, qui n'importe pas ce fichier Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
210 lines
6.5 KiB
Nix
210 lines
6.5 KiB
Nix
# SPDX-FileCopyrightText: 2026 Ryan Lahfa <ryan.lahfa@numerique.gouv.fr>
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
{
|
|
pkgs,
|
|
config,
|
|
lib,
|
|
...
|
|
}:
|
|
|
|
let
|
|
inherit (lib) mkEnableOption mkOption types;
|
|
|
|
cfg = config.security.anssi;
|
|
rulesets = import ./ruleset.nix { inherit lib; };
|
|
levelMapping = {
|
|
"minimal" = 0;
|
|
"intermediary" = 1;
|
|
"reinforced" = 2;
|
|
"high" = 3;
|
|
};
|
|
|
|
# rules.* must follow the format used earlier:
|
|
# {
|
|
# name = "...";
|
|
# category = "client" / "server" / "base";
|
|
# severity = "minimal" / "intermediary" / "reinforced" / "high";
|
|
# tags = [ "kernel" ... ];
|
|
# config = f: { ... };
|
|
# checkScript = pkgs: pkgs.writeShellScript ...
|
|
# }
|
|
|
|
# Look at all rules that are not enabled.
|
|
# Determine why they are not enabled:
|
|
# - if there's an exception for it, use it.
|
|
# - if it's excluded via a tag, use it.
|
|
# - if it's excluded via architecture, use it.
|
|
# - if it's out of the scope for the category, use it.
|
|
# - if it's out of the scope for the level, use it.
|
|
# - otherwise, mark it unknown.
|
|
# Determine why a rule is not enabled.
|
|
determineExclusionReason =
|
|
ruleId: rule:
|
|
if !cfg.rules.${ruleId}.enable then
|
|
# Check if it's excluded via a clear mechanism.
|
|
let
|
|
tagExclusions = lib.filter (tag: lib.elem tag (rule.tags or [ ])) cfg.excludes;
|
|
categoryExclusion = rule.category != "base" && rule.category != cfg.category;
|
|
levelExclusion = levelMapping.${rule.severity} > levelMapping.${cfg.level};
|
|
architectureExclusion =
|
|
if (rule.architectures or [ ]) != [ ] then
|
|
!lib.elem pkgs.stdenv.hostPlatform.linuxArch rule.architectures
|
|
else
|
|
false;
|
|
in
|
|
if tagExclusions != [ ] then
|
|
{
|
|
reason = "excluded by tag ${lib.concatStringsSep ", " tagExclusions}";
|
|
via = "tag";
|
|
exclusionTags = tagExclusions;
|
|
}
|
|
else if categoryExclusion then
|
|
{
|
|
reason = "out of scope for category '${cfg.category}' (${rule.category})";
|
|
via = "category";
|
|
requiredCategory = rule.category;
|
|
}
|
|
else if levelExclusion then
|
|
{
|
|
reason = "out of scope for level ${cfg.level} < ${rule.severity}";
|
|
via = "level";
|
|
requiredLevel = rule.severity;
|
|
}
|
|
else if architectureExclusion then
|
|
{
|
|
reason = "out of scope for architecture '${pkgs.stdenv.hostPlatform.linuxArch}'";
|
|
via = "architecture";
|
|
requiredArchitectures = rule.architectures or [ ];
|
|
}
|
|
else if lib.hasAttr ruleId cfg.exceptions then
|
|
{
|
|
reason = "excluded by explicit exception: ${cfg.exceptions.${ruleId}.rationale}";
|
|
via = "exception";
|
|
rationale = cfg.exceptions.${ruleId}.rationale;
|
|
}
|
|
else
|
|
{
|
|
reason = "could not understand why this rule was excluded, the user must have set the `enable = false;` forcibly via the module system bypassing the governance system. This is a non-compliant.";
|
|
via = "unknown";
|
|
}
|
|
else
|
|
null;
|
|
|
|
generateRuleEntry =
|
|
ruleId: rule:
|
|
{
|
|
enabled = cfg.rules.${ruleId}.enable;
|
|
anssiRef = cfg.rules.${ruleId}.anssiRef;
|
|
knownCheckScript = rule.checkScript pkgs;
|
|
}
|
|
// lib.optionalAttrs (cfg.exceptions ? ruleId) {
|
|
declaredExceptionRationale = cfg.exceptions.${ruleId}.rationale;
|
|
}
|
|
// lib.optionalAttrs (rule ? implementations) {
|
|
implementation = cfg.rules.${ruleId}.implementation;
|
|
};
|
|
|
|
complianceReport = {
|
|
"org.securix.anssi-compliance.v1" = {
|
|
guideVersion = "2.0";
|
|
rules = lib.mapAttrsToList (ruleId: rule: generateRuleEntry ruleId rule) rulesets;
|
|
targetLevel = cfg.level;
|
|
hostname = config.networking.hostName;
|
|
domain = config.networking.domain or null;
|
|
targetSystemCategory = cfg.category;
|
|
exclusions = lib.mapAttrs determineExclusionReason (
|
|
lib.filterAttrs (ruleId: _: !cfg.rules.${ruleId}.enable) rulesets
|
|
);
|
|
};
|
|
};
|
|
in
|
|
{
|
|
imports = [
|
|
# This will generate `security.anssi.rules.RXX.{anssiRef,enable,checkScript}` options and so on.
|
|
((import ./generator.nix).generateOptionHierarchy rulesets)
|
|
];
|
|
|
|
options.security.anssi = {
|
|
enable = mkEnableOption "ANSSI compliance for GNU/Linux systems (v2.0)";
|
|
|
|
level = mkOption {
|
|
type = types.enum [
|
|
"minimal"
|
|
"intermediary"
|
|
"reinforced"
|
|
"high"
|
|
];
|
|
default = "minimal";
|
|
description = "Select desired ANSSI compliance level.";
|
|
};
|
|
|
|
category = mkOption {
|
|
type = types.enum [
|
|
"base"
|
|
"client"
|
|
"server"
|
|
];
|
|
default = "base";
|
|
description = "Rule category to activate.";
|
|
};
|
|
|
|
excludes = mkOption {
|
|
type = types.listOf types.str;
|
|
default = [ ];
|
|
description = "Tags to exclude (e.g. [ \"kernel\" \"no-ipv6\" ])";
|
|
};
|
|
|
|
exceptions = mkOption {
|
|
type = types.attrsOf (
|
|
types.submodule (_: {
|
|
options.rationale = mkOption { type = types.lines; };
|
|
})
|
|
);
|
|
default = { };
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
system.build.complianceReport = complianceReport;
|
|
system.build.complianceReportDocument = pkgs.writers.writeJSON "anssi-compliance.json" complianceReport;
|
|
system.build.complianceCheckScript =
|
|
let
|
|
mkCheckSingularRule = ruleId: rule: ''
|
|
# Check for rule ${ruleId}
|
|
if [ -n "${rule.checkScript}" ]; then
|
|
result=$(bash -c "${rule.checkScript}")
|
|
results["${ruleId}"]=$result
|
|
else
|
|
results["${ruleId}"]="Check script not defined: cannot be checked."
|
|
fi
|
|
'';
|
|
in
|
|
pkgs.writeShellScriptBin "anssi-nixos-compliance-check" ''
|
|
declare -A results
|
|
|
|
${lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList mkCheckSingularRule (
|
|
lib.filterAttrs (_: rule: rule.enable) config.security.anssi.rules
|
|
)
|
|
)}
|
|
|
|
# Output the results in a matrix format
|
|
echo "ANSSI Compliance Check Results:"
|
|
for rule in "''${!results[@]}"; do
|
|
result="''${results[$rule]}"
|
|
if [[ "$result" =~ .*TODO.* ]]; then
|
|
printf "\033[33m%-40s : %s\033[0m\n" "$rule" "$result"
|
|
elif [[ "$result" =~ (fail|WARNING|DIVERGENCE|UNSET|error|not\ completed|incomplete) ]]; then
|
|
printf "\033[31m%-40s : %s\033[0m\n" "$rule" "$result"
|
|
else
|
|
printf "\033[32m%-40s : %s\033[0m\n" "$rule" "$result"
|
|
fi
|
|
done
|
|
'';
|
|
|
|
environment.systemPackages = [ config.system.build.complianceCheckScript ];
|
|
};
|
|
}
|