Cybersecurity-Projects/PROJECTS/beginner/firewall-rule-engine/src/main.v

270 lines
7.5 KiB
V

/*
©AngelaMos | 2026
main.v
CLI entry point with command dispatch and ruleset loading
Parses the first positional argument as a subcommand and fans out to
the appropriate handler. load/analyze/optimize/diff read a ruleset file
through load_ruleset, which auto-detects iptables vs nftables format
via parser.detect_format before delegating to the correct parser.
harden and export use V's flag module for option parsing (--services,
--iface, --format). Every command prints through the display module so
output formatting is consistent.
Key exports:
main - Entry point, dispatches to cmd_* handlers
load_ruleset - Reads a file, auto-detects format, returns a Ruleset
Connects to:
config/config.v - exit codes, app_name, version, default_services, default_iface
models/models.v - RuleSource for format selection in harden/export
parser/common.v - detect_format for auto-detection
parser/iptables.v - parse_iptables for iptables input
parser/nftables.v - parse_nftables for nftables input
analyzer/conflict.v - analyze_conflicts for the analyze command
analyzer/optimizer.v - suggest_optimizations for analyze/optimize commands
generator/generator.v - generate_hardened, export_ruleset
display/display.v - print_banner, print_summary, print_rule_table, print_findings, print_diff
*/
module main
import os
import flag
import src.config
import src.models
import src.parser
import src.analyzer
import src.generator
import term
import src.display
fn main() {
if os.args.len < 2 {
cmd_help()
exit(config.exit_usage_error)
}
command := os.args[1]
remaining := if os.args.len > 2 { os.args[2..] } else { []string{} }
match command {
'load', 'display' {
cmd_load(remaining)
}
'analyze' {
cmd_analyze(remaining)
}
'optimize' {
cmd_optimize(remaining)
}
'harden' {
cmd_harden(remaining)
}
'export' {
cmd_export(remaining)
}
'diff' {
cmd_diff(remaining)
}
'version', '--version', '-v' {
cmd_version()
}
'help', '--help', '-h' {
cmd_help()
}
else {
eprintln('unknown command: ${command}')
eprintln('Run "${config.app_name} help" for usage')
exit(config.exit_usage_error)
}
}
}
fn cmd_load(args []string) {
if args.len == 0 {
eprintln('usage: ${config.app_name} load <file>')
exit(config.exit_usage_error)
}
rs := load_ruleset(args[0]) or {
eprintln('${err}')
exit(config.exit_parse_error)
}
display.print_banner()
display.print_summary(rs)
display.print_rule_table(rs)
}
fn cmd_analyze(args []string) {
if args.len == 0 {
eprintln('usage: ${config.app_name} analyze <file>')
exit(config.exit_usage_error)
}
rs := load_ruleset(args[0]) or {
eprintln('${err}')
exit(config.exit_parse_error)
}
display.print_banner()
display.print_summary(rs)
println(term.bold(' Conflict Analysis'))
conflicts := analyzer.analyze_conflicts(rs)
display.print_findings(conflicts)
println(term.bold(' Optimization Suggestions'))
optimizations := analyzer.suggest_optimizations(rs)
display.print_findings(optimizations)
}
fn cmd_optimize(args []string) {
if args.len == 0 {
eprintln('usage: ${config.app_name} optimize <file>')
exit(config.exit_usage_error)
}
rs := load_ruleset(args[0]) or {
eprintln('${err}')
exit(config.exit_parse_error)
}
display.print_banner()
println(term.bold(' Optimization Suggestions'))
findings := analyzer.suggest_optimizations(rs)
display.print_findings(findings)
}
fn cmd_harden(args []string) {
mut fp := flag.new_flag_parser(args)
fp.application(config.app_name)
fp.description('Generate a hardened firewall ruleset')
services_str := fp.string('services', `s`, config.default_services.join(','), 'comma-separated list of services to allow')
iface := fp.string('iface', `i`, config.default_iface, 'public-facing network interface')
format_str := fp.string('format', `f`, 'iptables', 'output format (iptables or nftables)')
fp.finalize() or {
eprintln('${err}')
exit(config.exit_usage_error)
}
services := services_str.split(',').map(it.trim_space()).filter(it.len > 0)
out_format := match format_str.to_lower() {
'iptables' {
models.RuleSource.iptables
}
'nftables' {
models.RuleSource.nftables
}
else {
eprintln('invalid format: ${format_str} (use iptables or nftables)')
exit(config.exit_usage_error)
models.RuleSource.iptables
}
}
display.print_banner()
output := generator.generate_hardened(services, iface, out_format)
println(output)
}
fn cmd_export(args []string) {
mut fp := flag.new_flag_parser(args)
fp.application(config.app_name)
fp.description('Export ruleset in a different format')
format_str := fp.string('format', `f`, 'nftables', 'output format (iptables or nftables)')
remaining := fp.finalize() or {
eprintln('${err}')
exit(config.exit_usage_error)
[]string{}
}
if remaining.len == 0 {
eprintln('usage: ${config.app_name} export <file> --format <iptables|nftables>')
exit(config.exit_usage_error)
}
rs := load_ruleset(remaining[0]) or {
eprintln('${err}')
exit(config.exit_parse_error)
}
out_format := match format_str.to_lower() {
'nftables' {
models.RuleSource.nftables
}
'iptables' {
models.RuleSource.iptables
}
else {
eprintln('invalid format: ${format_str}')
exit(config.exit_usage_error)
models.RuleSource.iptables
}
}
output := generator.export_ruleset(rs, out_format)
println(output)
}
fn cmd_diff(args []string) {
if args.len < 2 {
eprintln('usage: ${config.app_name} diff <file1> <file2>')
exit(config.exit_usage_error)
}
left := load_ruleset(args[0]) or {
eprintln('${err}')
exit(config.exit_parse_error)
}
right := load_ruleset(args[1]) or {
eprintln('${err}')
exit(config.exit_parse_error)
}
display.print_banner()
display.print_diff(left, right)
}
fn cmd_version() {
println('${config.app_name} v${config.version}')
}
fn cmd_help() {
println('${config.app_name} v${config.version} - Firewall Rule Engine')
println('')
println('USAGE:')
println(' ${config.app_name} <command> [options]')
println('')
println('COMMANDS:')
println(' load <file> Load and display a ruleset')
println(' analyze <file> Run conflict detection and optimization analysis')
println(' optimize <file> Show optimization suggestions')
println(' harden Generate a hardened ruleset')
println(' export <file> Convert ruleset between iptables/nftables formats')
println(' diff <f1> <f2> Compare two rulesets')
println(' version Show version')
println(' help Show this help')
println('')
println('HARDEN OPTIONS:')
println(' -s, --services Services to allow (default: ssh,http,https)')
println(' -i, --iface Public interface (default: eth0)')
println(' -f, --format Output format: iptables or nftables (default: iptables)')
println('')
println('EXPORT OPTIONS:')
println(' -f, --format Target format: iptables or nftables (default: nftables)')
println('')
println('EXAMPLES:')
println(' ${config.app_name} load rules.txt')
println(' ${config.app_name} analyze /etc/iptables.rules')
println(' ${config.app_name} harden -s ssh,http,https -f nftables')
println(' ${config.app_name} export rules.txt -f nftables')
println(' ${config.app_name} diff old.rules new.rules')
}
fn load_ruleset(path string) !models.Ruleset {
if !os.exists(path) {
return error('file not found: ${path}')
}
content := os.read_file(path) or { return error('cannot read file: ${path}') }
fmt := parser.detect_format(content)!
return match fmt {
.iptables { parser.parse_iptables(content)! }
.nftables { parser.parse_nftables(content)! }
}
}