master
go 159 lines 4.57 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package main
4
5 import (
6 "flag"
7 "fmt"
8 "net/url"
9 "os"
10 "path/filepath"
11 "strings"
12 )
13
14 func main() {
15 var configPath string
16 var outputDir string
17 var asnFlags familyFlagValues
18 var geoFlags familyFlagValues
19 var noASN bool
20 var noGeo bool
21
22 asnFlags.family = sourceFamilyASN
23 geoFlags.family = sourceFamilyGeo
24
25 flag.StringVar(&configPath, "config", "", "path to topology IP intelligence YAML config")
26 flag.StringVar(&outputDir, "output-dir", "", "override output directory")
27 flag.Var(&asnFlags, "asn", "repeatable ASN source token: provider:artifact[@format]")
28 flag.Var(&geoFlags, "geo", "repeatable GEO source token: provider:artifact[@format]")
29 flag.BoolVar(&noASN, "no-asn", false, "disable ASN output and remove any stale ASN file")
30 flag.BoolVar(&noGeo, "no-geo", false, "disable GEO output and remove any stale GEO file")
31 flag.Parse()
32
33 cfg, loadedPath, err := loadConfig(configPath)
34 if err != nil {
35 fmt.Fprintf(os.Stderr, "topology-ip-intel-downloader: failed to load config: %v\n", err)
36 os.Exit(1)
37 }
38 if outputDir = strings.TrimSpace(outputDir); outputDir != "" {
39 cfg.output.directory = outputDir
40 }
41
42 cfg.sources = applyFamilyOverride(cfg.sources, sourceFamilyASN, asnFlags.sources, noASN)
43 cfg.sources = applyFamilyOverride(cfg.sources, sourceFamilyGeo, geoFlags.sources, noGeo)
44
45 if err := cfg.normalizeAndValidate(); err != nil {
46 fmt.Fprintf(os.Stderr, "topology-ip-intel-downloader: invalid config: %v\n", err)
47 os.Exit(1)
48 }
49
50 printExecutionPlan(cfg)
51
52 dl := newDownloader(cfg.http)
53 asnRanges, geoRanges, sourceRefs, err := loadRanges(cfg, dl)
54 if err != nil {
55 fmt.Fprintf(os.Stderr, "topology-ip-intel-downloader: failed to download/parse source data: %v\n", err)
56 os.Exit(1)
57 }
58
59 if err := writeOutputs(cfg, asnRanges, geoRanges, sourceRefs); err != nil {
60 fmt.Fprintf(os.Stderr, "topology-ip-intel-downloader: failed to write outputs: %v\n", err)
61 os.Exit(1)
62 }
63
64 asnPath := filepath.Join(cfg.output.directory, cfg.output.asnFile)
65 geoPath := filepath.Join(cfg.output.directory, cfg.output.geoFile)
66 metadataPath := filepath.Join(cfg.output.directory, cfg.output.metadataFile)
67 if loadedPath != "" {
68 fmt.Printf("updated IP intelligence databases using config %s\n", loadedPath)
69 } else {
70 fmt.Printf("updated IP intelligence databases using built-in defaults\n")
71 }
72 if cfg.hasFamily(sourceFamilyASN) {
73 fmt.Printf("asn_mmdb=%s\n", asnPath)
74 } else {
75 fmt.Printf("asn_mmdb=disabled\n")
76 }
77 if cfg.hasFamily(sourceFamilyGeo) {
78 fmt.Printf("geo_mmdb=%s\n", geoPath)
79 } else {
80 fmt.Printf("geo_mmdb=disabled\n")
81 }
82 fmt.Printf("metadata=%s\n", metadataPath)
83 fmt.Printf("asn_ranges=%d geo_ranges=%d\n", len(asnRanges), len(geoRanges))
84 }
85
86 func applyFamilyOverride(
87 current []sourceEntry,
88 family string,
89 override []sourceEntry,
90 disable bool,
91 ) []sourceEntry {
92 if !disable && len(override) == 0 {
93 return current
94 }
95
96 out := make([]sourceEntry, 0, len(current)+len(override))
97 for _, source := range current {
98 if source.family != family {
99 out = append(out, source)
100 }
101 }
102 if disable {
103 return out
104 }
105 out = append(out, override...)
106 return out
107 }
108
109 func printExecutionPlan(cfg config) {
110 fmt.Printf("effective source plan:\n")
111 printFamilyPlan(sourceFamilyASN, cfg.familySources(sourceFamilyASN))
112 printFamilyPlan(sourceFamilyGeo, cfg.familySources(sourceFamilyGeo))
113 fmt.Printf("output actions:\n")
114 if cfg.hasFamily(sourceFamilyASN) {
115 fmt.Printf("- write %s\n", cfg.output.asnFile)
116 } else {
117 fmt.Printf("- remove %s\n", cfg.output.asnFile)
118 }
119 if cfg.hasFamily(sourceFamilyGeo) {
120 fmt.Printf("- write %s\n", cfg.output.geoFile)
121 } else {
122 fmt.Printf("- remove %s\n", cfg.output.geoFile)
123 }
124 fmt.Printf("- write %s\n", cfg.output.metadataFile)
125 }
126
127 func printFamilyPlan(family string, sources []sourceEntry) {
128 label := strings.ToUpper(family)
129 if len(sources) == 0 {
130 fmt.Printf("%s sources: none\n", label)
131 return
132 }
133 fmt.Printf("%s sources (first wins):\n", label)
134 for i, source := range sources {
135 fmt.Printf("- %d. %s\n", i+1, sourceDisplayName(source))
136 }
137 }
138
139 func sourceDisplayName(source sourceEntry) string {
140 base := fmt.Sprintf("%s:%s", source.provider, source.artifact)
141 if source.format != "" {
142 base = fmt.Sprintf("%s@%s", base, source.format)
143 }
144 if source.path != "" {
145 return fmt.Sprintf("%s path=%s", base, source.path)
146 }
147 if source.url != "" {
148 return fmt.Sprintf("%s url=%s", base, redactURLForDisplay(source.url))
149 }
150 return base
151 }
152
153 func redactURLForDisplay(raw string) string {
154 parsed, err := url.Parse(raw)
155 if err != nil || parsed.Host == "" {
156 return "<redacted>"
157 }
158 return fmt.Sprintf("%s://%s/<redacted>", parsed.Scheme, parsed.Host)
159 }