|
|
|
@ -1,7 +1,9 @@
|
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
|
|
from argparse import ArgumentParser, FileType
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from enum import Enum
|
|
|
|
|
from graphlib import TopologicalSorter
|
|
|
|
|
from pydantic import (
|
|
|
|
|
BaseModel,
|
|
|
|
|
Extra,
|
|
|
|
@ -43,31 +45,43 @@ class PortRange(str):
|
|
|
|
|
return range(start, end)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ===== First pass: Zones =====
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Zones
|
|
|
|
|
class ZoneName(str):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ZoneEntries(RestrictiveBaseModel):
|
|
|
|
|
addrs: list[IPvAnyNetwork] | None
|
|
|
|
|
files: list[FilePath] | None
|
|
|
|
|
zones: list[ZoneName] | None
|
|
|
|
|
@dataclass
|
|
|
|
|
class Zone:
|
|
|
|
|
addrs: set[IPvAnyNetwork]
|
|
|
|
|
negate: bool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Zones: Parsing YAML
|
|
|
|
|
class ZoneYAML(RestrictiveBaseModel):
|
|
|
|
|
addrs: set[IPvAnyNetwork] = set()
|
|
|
|
|
files: set[FilePath] = set()
|
|
|
|
|
negate: bool = False
|
|
|
|
|
zones: set[ZoneName] = set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Zone(RestrictiveBaseModel):
|
|
|
|
|
name: ZoneName
|
|
|
|
|
exclude: ZoneEntries | None
|
|
|
|
|
include: ZoneEntries | None
|
|
|
|
|
# Zones: Graph resolver
|
|
|
|
|
def convert_to_zone_and_deps(zone_yaml: ZoneYAML) -> tuple[Zone, list[ZoneName]]:
|
|
|
|
|
return (Zone(addrs=zone_yaml.addrs, negate=zone_yaml.negate), zone_yaml.zones)
|
|
|
|
|
|
|
|
|
|
@root_validator()
|
|
|
|
|
def validate_mutually_exactly_one(cls, values):
|
|
|
|
|
if values.get("exclude") and values.get("include"):
|
|
|
|
|
raise ValueError("exclude and include are mutually exclusive")
|
|
|
|
|
|
|
|
|
|
if values.get("exclude") is None and values.get("include") is None:
|
|
|
|
|
raise ValueError("exactly one of exclude and include must be set")
|
|
|
|
|
def resolve_zones(zones):
|
|
|
|
|
zones = { name: convert_to_zone_and_deps(ZoneYAML(**zone)) for (name, zone) in zones.items() }
|
|
|
|
|
zone_name = { name: set(zones) for (name, (_, zones)) in zones.items() }
|
|
|
|
|
|
|
|
|
|
return values
|
|
|
|
|
print(zones)
|
|
|
|
|
|
|
|
|
|
for name in TopologicalSorter(zone_name).static_order():
|
|
|
|
|
print(name)
|
|
|
|
|
|
|
|
|
|
# TODO: Check negation inclusion
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Blacklist
|
|
|
|
@ -115,7 +129,8 @@ class Rule(RestrictiveBaseModel):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ForwardRule(Rule):
|
|
|
|
|
dest: ZoneEntries | None
|
|
|
|
|
# dest: ZoneEntries | None
|
|
|
|
|
dest: None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Filter(RestrictiveBaseModel):
|
|
|
|
@ -131,13 +146,13 @@ class SNat(RestrictiveBaseModel):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Nat(RestrictiveBaseModel):
|
|
|
|
|
src: ZoneEntries | None
|
|
|
|
|
# src: ZoneEntries | None
|
|
|
|
|
src: None
|
|
|
|
|
snat: SNat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Root model
|
|
|
|
|
class Firewall(RestrictiveBaseModel):
|
|
|
|
|
zones: list[Zone] = []
|
|
|
|
|
blacklist: BlackList | None
|
|
|
|
|
reverse_path_filter: ReversePathFilter | None
|
|
|
|
|
filter: Filter | None
|
|
|
|
@ -149,9 +164,14 @@ def main():
|
|
|
|
|
parser.add_argument("file", type=FileType("r"), help="YAML rule file")
|
|
|
|
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
contents = safe_load(args.file)
|
|
|
|
|
|
|
|
|
|
zones = resolve_zones(contents.pop("zones"))
|
|
|
|
|
print(zones)
|
|
|
|
|
|
|
|
|
|
rules = Firewall(**safe_load(args.file))
|
|
|
|
|
exit(0)
|
|
|
|
|
|
|
|
|
|
rules = Firewall(**contents)
|
|
|
|
|
print(rules)
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|