• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

hknutzen / Netspoc-Approve / 16674559403

01 Aug 2025 12:03PM UTC coverage: 98.531% (-0.07%) from 98.6%
16674559403

push

github

hknutzen
CHANGELOG file

5230 of 5308 relevant lines covered (98.53%)

1.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.5
/go/pkg/cisco/parse.go
1
package cisco
2

3
import (
4
        "bytes"
5
        "fmt"
6
        "net/netip"
7
        "path"
8
        "regexp"
9
        "slices"
10
        "strconv"
11
        "strings"
12
        "unicode"
13

14
        "github.com/hknutzen/Netspoc-Approve/go/pkg/errlog"
15
)
16

17
type Config struct {
18
        // prefix -> name -> commands with same prefix and name
19
        lookup objLookup
20
        isRaw  bool
21
}
22

23
type objLookup map[string]map[string][]*cmd
24

25
type cmdType struct {
26
        prefix   string   // e.g. "crypto map"
27
        template []string // e.g. ["$NAME", "$SEQ", "match", "address", "$REF"]
28
        ref      []string // Referenced prefixes, e.g. ["access-list"]
29
        ignore   bool     // Matching command is ignored.
30
        sub      []*cmdType
31
        // Use "clear configure PREFIX NAME [SEQ]" to remove the complete
32
        // command
33
        clearConf bool
34
        // Do not change object on device but try to find a matching
35
        // command on device.
36
        simpleObj bool
37
        anchor    bool
38
        fixedName bool
39
}
40

41
type cmd struct {
42
        typ   *cmdType
43
        ready bool // cmd from Netspoc was found on or transferred to device
44
        // cmd on device is referenced and must not be deleted
45
        // or is marked as already deleted.
46
        needed    bool
47
        toDelete  bool // Remove cmd on device if it is not needed
48
        anchor    bool
49
        fixedName bool
50
        append    bool // Command was found after [APPEND] marker in raw file
51

52
        orig string // e.g. "crypto map abc 10 match address xyz"
53
        // "*" and `"` of template are only used for matching,
54
        // but 'parsed' contains original value.
55
        parsed   string   // e.g. "crypto map $NAME $SEQ match address $REF"
56
        name     string   // Value of $NAME, e.g. "abc"
57
        seq      int      // Value of $SEQ,  e.g. 10
58
        ref      []string // Values of $REF, e.g. ["xyz"]
59
        sub      []*cmd
60
        subCmdOf *cmd
61
}
62

63
type parser struct {
64
        cmdDescr  []*cmdType
65
        prefixMap map[string]*cmdLookup
66
        Model     string
67
}
68

69
func (s *State) SetupParser(cmdInfo string) {
1✔
70
        p := &parser{}
1✔
71
        p.setupCmdDescr(cmdInfo)
1✔
72
        p.setupLookup()
1✔
73
        s.parser = p
1✔
74
}
1✔
75

76
func (p *parser) ParseConfig(data []byte, fName string) (*Config, error) {
1✔
77
        if p.Model == "IOS" {
2✔
78
                data = removeBanner(data)
1✔
79
        }
1✔
80

81
        // prefix -> name -> commands with same prefix and name
82
        lookup := make(objLookup)
1✔
83
        // Remember previous toplevel command where subcommands are added.
1✔
84
        var prev *cmd
1✔
85
        // Allow uncommon indentation only at first subcommand.
1✔
86
        isFirstSubCmd := false
1✔
87
        // Indentation count of subcommand.
1✔
88
        indent := 1
1✔
89
        // Mark commands found after [APPEND] marker.
1✔
90
        isAppend := false
1✔
91
        isRaw := path.Ext(fName) == ".raw"
1✔
92
        for len(data) > 0 {
2✔
93
                first, rest, _ := bytes.Cut(data, []byte("\n"))
1✔
94
                data = rest
1✔
95
                line := string(first)
1✔
96
                // Remove whitespace at end of line in manually created raw file.
1✔
97
                line = strings.TrimRightFunc(line, unicode.IsSpace)
1✔
98
                if line == "" || line[0] == '!' {
2✔
99
                        continue
1✔
100
                }
101
                if line == "[APPEND]" {
2✔
102
                        isAppend = true
1✔
103
                        continue
1✔
104
                }
105
                if line[0] != ' ' {
2✔
106
                        // Handle toplevel command.
1✔
107
                        c := p.lookupCmd(line)
1✔
108
                        prev = c // Set to next command or nil.
1✔
109
                        isFirstSubCmd = true
1✔
110
                        if c == nil {
2✔
111
                                if isRaw {
2✔
112
                                        return nil, fmt.Errorf("Unexpected command:\n>>%s<<", line)
1✔
113
                                }
1✔
114
                        } else {
1✔
115
                                p := c.typ.prefix
1✔
116
                                m := lookup[p]
1✔
117
                                if m == nil {
2✔
118
                                        m = make(map[string][]*cmd)
1✔
119
                                        lookup[p] = m
1✔
120
                                }
1✔
121
                                c.append = isAppend
1✔
122
                                m[c.name] = append(m[c.name], c)
1✔
123
                        }
124
                } else if prev != nil {
2✔
125
                        // Handle sub command of non ignored command.
1✔
126
                        getIndent := func() int {
2✔
127
                                return strings.IndexFunc(line, func(c rune) bool { return c != ' ' })
2✔
128
                        }
129
                        if isFirstSubCmd {
2✔
130
                                // Allow higher indentation at first subcommand.
1✔
131
                                // This applies to following subcommands as well.
1✔
132
                                isFirstSubCmd = false
1✔
133
                                indent = getIndent()
1✔
134
                        } else {
2✔
135
                                if getIndent() < indent {
2✔
136
                                        return nil,
1✔
137
                                                fmt.Errorf("Bad indentation in subcommands:\n"+
1✔
138
                                                        ">>%s<<\n>>%s<<",
1✔
139
                                                        strings.Repeat(" ", indent)+prev.sub[0].parsed, line)
1✔
140
                                }
1✔
141
                        }
142
                        line = line[indent:]
1✔
143
                        // Ignore sub-sub command.
1✔
144
                        if line[0] == ' ' {
2✔
145
                                continue
1✔
146
                        }
147
                        // Get arguments.  Use strings.Fields, not strings.Split to
148
                        // remove extra indentation between arguments.
149
                        // Example:  "map-name  memberOf ..."
150
                        words := strings.Fields(line)
1✔
151
                        if c := matchCmd("", words, prev.typ.sub); c != nil {
2✔
152
                                prev.sub = append(prev.sub, c)
1✔
153
                                c.subCmdOf = prev
1✔
154
                                c.append = isAppend
1✔
155
                        }
1✔
156
                }
157
        }
158
        postprocessParsed(lookup)
1✔
159
        err := p.checkReferences(lookup, isRaw)
1✔
160
        return &Config{lookup: lookup, isRaw: isRaw}, err
1✔
161
}
162

163
func (p *parser) checkReferences(lookup objLookup, isRaw bool) error {
1✔
164
        for _, m := range lookup {
2✔
165
                for _, cmdList := range m {
2✔
166
                        check := func(c *cmd) error {
2✔
167
                                for i, name := range c.ref {
2✔
168
                                        prefix := c.typ.ref[i]
1✔
169
                                        if _, found := lookup[prefix][name]; !found {
2✔
170
                                                if vl := defaultObjects[[2]string{prefix, name}]; vl != nil {
2✔
171
                                                        p.addDefaultObject(lookup, prefix, name, vl)
1✔
172
                                                } else if !isRaw && prefix == "ip access-list extended" {
3✔
173
                                                        // Remove reference to unknown ACL.
1✔
174
                                                        c.ref = nil
1✔
175
                                                        c.parsed = c.orig
1✔
176
                                                        break
1✔
177
                                                } else {
1✔
178
                                                        return fmt.Errorf("'%s' references unknown '%s %s'",
1✔
179
                                                                c.orig, prefix, name)
1✔
180
                                                }
1✔
181
                                        }
182
                                }
183
                                return nil
1✔
184
                        }
185
                        for _, c := range cmdList {
2✔
186
                                if err := check(c); err != nil {
2✔
187
                                        return err
1✔
188
                                }
1✔
189
                                for _, sc := range c.sub {
2✔
190
                                        if err := check(sc); err != nil {
2✔
191
                                                return err
1✔
192
                                        }
1✔
193
                                }
194
                        }
195
                }
196
        }
197
        return nil
1✔
198
}
199

200
var defaultObjects = map[[2]string][]string{
201
        {"group-policy", "DfltGrpPolicy"}: {"internal"},
202
        {"tunnel-group", "DefaultL2LGroup"}: {
203
                "type ipsec-l2l", "general-attributes"},
204
        {"tunnel-group", "DefaultRAGroup"}: {
205
                "type remote-access", "general-attributes"},
206
        {"tunnel-group", "DefaultWEBVPNGroup"}: {"type webvpn", "general-attributes"},
207
}
208

209
func (p *parser) addDefaultObject(lookup objLookup, prefix, name string, vl []string) {
1✔
210
        m := lookup[prefix]
1✔
211
        if m == nil {
2✔
212
                m = make(map[string][]*cmd)
1✔
213
                lookup[prefix] = m
1✔
214
        }
1✔
215
OBJ:
1✔
216
        for _, arg := range vl {
2✔
217
                c := p.lookupCmd(prefix + " " + name + " " + arg)
1✔
218
                l := m[name]
1✔
219
                // Do only add, if not already parsed previously.
1✔
220
                for _, c2 := range l {
2✔
221
                        if c2.parsed == c.parsed {
2✔
222
                                c2.fixedName = true
1✔
223
                                c2.anchor = true
1✔
224
                                continue OBJ
1✔
225
                        }
226
                }
227
                c.fixedName = true
1✔
228
                c.anchor = true
1✔
229
                l = append([]*cmd{c}, l...)
1✔
230
                m[name] = l
1✔
231
        }
232
}
233

234
// Add definitions of default group-policy and default tunnel-groups.
235
func (p *parser) addDefaults(cf *Config) {
1✔
236
        lookup := cf.lookup
1✔
237
        for k, vl := range defaultObjects {
2✔
238
                prefix, name := k[0], k[1]
1✔
239
                if p.prefixMap[prefix] != nil { // Ignore ASA defaults for IOS.
2✔
240
                        p.addDefaultObject(lookup, prefix, name, vl)
1✔
241
                }
1✔
242
        }
243
}
244

245
type header struct {
246
        anchor    bool
247
        fixedName bool
248
        clearConf bool
249
        simpleObj bool
250
}
251

252
// Initialize cmdDescr from lines in cmdInfo.
253
func (p *parser) setupCmdDescr(info string) {
1✔
254
        toParse := info
1✔
255
        sectHead := header{}
1✔
256
        for toParse != "" {
2✔
257
                store := &p.cmdDescr
1✔
258
                line, rest, _ := strings.Cut(toParse, "\n")
1✔
259
                toParse = rest
1✔
260
                line = strings.TrimRight(line, " \t\r")
1✔
261
                if line == "" {
2✔
262
                        sectHead = header{}
1✔
263
                        continue
1✔
264
                }
265
                isSubCmd := false
1✔
266
                ignore := false
1✔
267
                switch line[0] {
1✔
268
                case '#':
1✔
269
                        continue
1✔
270
                case '[':
1✔
271
                        sectHead = parseHeader(line)
1✔
272
                        continue
1✔
273
                case ' ':
1✔
274
                        line = line[1:]
1✔
275
                        if len(*store) == 0 {
1✔
276
                                panic(fmt.Errorf("first line of cmdInfo must not be indented"))
×
277
                        }
278
                        if line[0] == ' ' {
1✔
279
                                panic(fmt.Errorf(
×
280
                                        "only indentation with one space supported in cmdInfo"))
×
281
                        }
282
                        prev := (*store)[len(*store)-1]
1✔
283
                        store = &prev.sub
1✔
284
                        isSubCmd = true
1✔
285
                }
286
                if line[0] == '!' {
2✔
287
                        line = line[1:]
1✔
288
                        ignore = true
1✔
289
                        if line == "" {
1✔
290
                                panic(fmt.Errorf("invalid line with only '!' in cmdInfo"))
×
291
                        }
292
                }
293
                parts := strings.Fields(line)
1✔
294
                prefix := ""
1✔
295
                if !isSubCmd {
2✔
296
                        prefix = strings.ReplaceAll(parts[0], "_", " ")
1✔
297
                        parts = parts[1:]
1✔
298
                }
1✔
299
                template := parts
1✔
300
                ref := []string{}
1✔
301
                for i, val := range template {
2✔
302
                        if val == "*" && i != len(template)-1 {
1✔
303
                                panic(fmt.Errorf("* must only be used at end of line in cmdInfo"))
×
304
                        }
305
                        if !(val[0] == '$' && len(val) > 1) {
2✔
306
                                continue
1✔
307
                        }
308
                        switch v := val[1:]; v {
1✔
309
                        case "NAME", "SEQ":
1✔
310
                                continue
1✔
311
                        default:
1✔
312
                                ref = append(ref, strings.ReplaceAll(v, "_", " "))
1✔
313
                                template[i] = "$REF"
1✔
314
                        }
315
                }
316
                descr := &cmdType{
1✔
317
                        prefix:   prefix,
1✔
318
                        template: template,
1✔
319
                        ref:      ref,
1✔
320
                        ignore:   ignore,
1✔
321
                }
1✔
322
                if sectHead.anchor {
2✔
323
                        descr.anchor = true
1✔
324
                }
1✔
325
                if sectHead.fixedName {
2✔
326
                        descr.fixedName = true
1✔
327
                }
1✔
328
                if sectHead.clearConf {
2✔
329
                        descr.clearConf = true
1✔
330
                }
1✔
331
                if sectHead.simpleObj {
2✔
332
                        descr.simpleObj = true
1✔
333
                }
1✔
334
                *store = append(*store, descr)
1✔
335
        }
336
}
337

338
func parseHeader(line string) header {
1✔
339
        h := header{}
1✔
340
        line = strings.Trim(line, "[]")
1✔
341
        for _, w := range strings.Split(line, ",") {
2✔
342
                w = strings.TrimSpace(w)
1✔
343
                switch w {
1✔
344
                case "ANCHOR":
1✔
345
                        h.anchor = true
1✔
346
                case "FIXED_NAME":
1✔
347
                        h.fixedName = true
1✔
348
                case "SIMPLE_OBJ":
1✔
349
                        h.simpleObj = true
1✔
350
                case "CLEAR_CONF":
1✔
351
                        h.clearConf = true
1✔
352
                default:
×
353
                        panic(fmt.Errorf("Invalid token %q in section header of cmdInfo", w))
×
354
                }
355
        }
356
        return h
1✔
357
}
358

359
type cmdLookup struct {
360
        prefixMap map[string]*cmdLookup
361
        descrList []*cmdType
362
}
363

364
// Fill prefixMap with commands from cmdDescr.
365
func (p *parser) setupLookup() {
1✔
366
        p.prefixMap = make(map[string]*cmdLookup)
1✔
367
        for _, descr := range p.cmdDescr {
2✔
368
                words := strings.Split(descr.prefix, " ")
1✔
369
                m := p.prefixMap
1✔
370
                for {
2✔
371
                        w1 := words[0]
1✔
372
                        words = words[1:]
1✔
373
                        cl := m[w1]
1✔
374
                        if cl == nil {
2✔
375
                                cl = &cmdLookup{}
1✔
376
                                m[w1] = cl
1✔
377
                        }
1✔
378
                        if len(words) == 0 {
2✔
379
                                if cl.prefixMap != nil {
1✔
380
                                        panic(fmt.Errorf("inconsistent prefix in cmdInfo: %s", words))
×
381
                                }
382
                                cl.descrList = append(cl.descrList, descr)
1✔
383
                                break
1✔
384
                        }
385
                        if cl.descrList != nil {
1✔
386
                                panic(fmt.Errorf("inconsistent prefix in cmdInfo: %s", words))
×
387
                        }
388
                        if cl.prefixMap == nil {
2✔
389
                                cl.prefixMap = make(map[string]*cmdLookup)
1✔
390
                        }
1✔
391
                        m = cl.prefixMap
1✔
392
                }
393
        }
394
}
395

396
func (p *parser) lookupCmd(line string) *cmd {
1✔
397
        words := strings.Split(line, " ")
1✔
398
        m := p.prefixMap
1✔
399
        for i, w1 := range words {
2✔
400
                cl := m[w1]
1✔
401
                if cl == nil {
2✔
402
                        return nil
1✔
403
                }
1✔
404
                if l := cl.descrList; l != nil {
2✔
405
                        prefix := strings.Join(words[:i+1], " ")
1✔
406
                        args := words[i+1:]
1✔
407
                        return matchCmd(prefix, args, l)
1✔
408
                }
1✔
409
                m = cl.prefixMap
1✔
410
        }
411
        return nil
1✔
412
}
413

414
func matchCmd(prefix string, words []string, l []*cmdType) *cmd {
1✔
415
DESCR:
1✔
416
        for _, descr := range l {
2✔
417
                args := words
1✔
418
                var parsed []string
1✔
419
                var name string
1✔
420
                var seq int
1✔
421
                var ref []string
1✔
422
        TEMPLATE:
1✔
423
                for _, token := range descr.template {
2✔
424
                        if len(args) == 0 {
2✔
425
                                continue DESCR
1✔
426
                        }
427
                        w := args[0]
1✔
428
                        switch token {
1✔
429
                        case "$NAME":
1✔
430
                                name = w
1✔
431
                                parsed = append(parsed, token)
1✔
432
                        case "$SEQ":
1✔
433
                                num, err := strconv.ParseUint(w, 10, 0)
1✔
434
                                if err != nil {
2✔
435
                                        continue DESCR
1✔
436
                                }
437
                                seq = int(num)
1✔
438
                                parsed = append(parsed, token)
1✔
439
                        case "$REF":
1✔
440
                                ref = append(ref, w)
1✔
441
                                parsed = append(parsed, token)
1✔
442
                        case `"`:
1✔
443
                                strg := ""
1✔
444
                                if w[0] == '"' {
2✔
445
                                        for j, w2 := range args {
2✔
446
                                                if strings.HasSuffix(w2, `"`) && !strings.HasSuffix(w2, `\"`) {
2✔
447
                                                        strg = strings.Join(args[:j+1], " ")
1✔
448
                                                        args = args[j:]
1✔
449
                                                        break
1✔
450
                                                }
451
                                        }
452
                                } else {
1✔
453
                                        strg = `"` + w + `"`
1✔
454
                                }
1✔
455
                                if strg == "" {
2✔
456
                                        panic(fmt.Errorf("Incomplete string in: %v", words))
1✔
457
                                }
458
                                parsed = append(parsed, strg)
1✔
459
                        case "*":
1✔
460
                                parsed = append(parsed, strings.Join(args, " "))
1✔
461
                                args = nil
1✔
462
                                break TEMPLATE
1✔
463
                        default:
1✔
464
                                if token != w {
2✔
465
                                        continue DESCR
1✔
466
                                }
467
                                parsed = append(parsed, w)
1✔
468
                        }
469
                        args = args[1:]
1✔
470
                }
471
                if len(args) > 0 {
2✔
472
                        continue
1✔
473
                }
474
                if descr.ignore {
2✔
475
                        return nil
1✔
476
                }
1✔
477
                if prefix != "" {
2✔
478
                        words = append([]string{prefix}, words...)
1✔
479
                        parsed = append([]string{prefix}, parsed...)
1✔
480
                }
1✔
481
                c := &cmd{
1✔
482
                        typ:    descr,
1✔
483
                        orig:   strings.Join(words, " "),
1✔
484
                        parsed: strings.Join(parsed, " "),
1✔
485
                        name:   name,
1✔
486
                        seq:    seq,
1✔
487
                        ref:    ref,
1✔
488
                }
1✔
489
                return c
1✔
490
        }
491
        return nil
1✔
492
}
493

494
func postprocessParsed(lookup objLookup) {
1✔
495
        // In access-list, replace "object-group NAME" by "$REF" in cmd.parsed
1✔
496
        // and add "NAME" to cmd.ref .
1✔
497
        for _, l := range lookup["access-list"] {
2✔
498
                for _, c := range l {
2✔
499
                        postprocessASAACL(c)
1✔
500
                        // access-list may reference up to five object-groups.
1✔
501
                        c.typ.ref = []string{
1✔
502
                                "object-group", "object-group", "object-group",
1✔
503
                                "object-group", "object-group",
1✔
504
                        }
1✔
505
                }
1✔
506
        }
507
        for _, l := range lookup["ip access-list extended"] {
2✔
508
                for _, c := range l[0].sub {
2✔
509
                        postprocessIOSACL(c)
1✔
510
                }
1✔
511
        }
512
        // Move crypto map interface commands to different prefix for
513
        // easier subsequent processing.
514
        if l := lookup["crypto map"][""]; l != nil {
2✔
515
                lookup["crypto map interface"] = map[string][]*cmd{"": l}
1✔
516
                delete(lookup["crypto map"], "")
1✔
517
        }
1✔
518
        // NAME in commands
519
        // - "crypto_dynamic-map $NAME $SEQ set ikev1 transform-set NAME ..." and
520
        // - "crypto_map $NAME $SEQ set ikev1 transform-set NAME ..."
521
        // may reference up to 11 $crypto_ipsec_ikev1_transform-set
522
        // NAME in commands
523
        // - "crypto_dynamic-map $NAME $SEQ set ikev2 ipsec-proposal NAME ..." and
524
        // - "crypto_map $NAME $SEQ set ikev2 ipsec-proposal NAME ..."
525
        // may reference up to 11 $crypto_ipsec_ikev2_ipsec-proposal
526
        setTransRef := func(prefix, part string) {
2✔
527
                cmdPart := " set " + part + " "
1✔
528
                for _, l := range lookup[prefix] {
2✔
529
                        for _, c := range l {
2✔
530
                                if def, names, found := strings.Cut(c.parsed, cmdPart); found {
2✔
531
                                        nl := strings.Fields(names)
1✔
532
                                        c.ref = nl
1✔
533
                                        c.parsed =
1✔
534
                                                def + cmdPart + strings.Repeat("$REF ", len(nl)-1) + "$REF"
1✔
535
                                        def := "crypto ipsec " + part
1✔
536
                                        c.typ.ref =
1✔
537
                                                []string{def, def, def, def, def, def, def, def, def, def, def}
1✔
538
                                }
1✔
539
                        }
540
                }
541
        }
542
        setTransRef("crypto map", "ikev1 transform-set")
1✔
543
        setTransRef("crypto map", "ikev2 ipsec-proposal")
1✔
544
        setTransRef("crypto dynamic-map", "ikev1 transform-set")
1✔
545
        setTransRef("crypto dynamic-map", "ikev2 ipsec-proposal")
1✔
546

1✔
547
        // Strip default value group14 from "crypto [dynamic-]map set pfs group14"
1✔
548
        // The default is group2 for releases prior to 9.13,
1✔
549
        // and group14 for release 9.13 and later.
1✔
550
        // Hence this is only safe,
1✔
551
        // - for versions 9.13 and later
1✔
552
        // - for versions prior to 9.13 if group2 isn't used.
1✔
553
        stripPFSDefault := func(prefix string) {
2✔
554
                for _, l := range lookup[prefix] {
2✔
555
                        for _, c := range l {
2✔
556
                                if strings.HasSuffix(c.parsed, "$NAME $SEQ set pfs group14") {
2✔
557
                                        c.parsed = strings.TrimSuffix(c.parsed, " group14")
1✔
558
                                }
1✔
559
                        }
560
                }
561
        }
562
        stripPFSDefault("crypto map")
1✔
563
        stripPFSDefault("crypto dynamic-map")
1✔
564

1✔
565
        // Normalize routes:
1✔
566
        // Strip trailing [metric] in
1✔
567
        // - "route if_name ip_address netmask gateway_ip [metric]"
1✔
568
        // - "ipv6 route if_name destination next_hop_ipv6_addr [metric]"
1✔
569
        stripMetric := func(prefix string) {
2✔
570
                for _, l := range lookup[prefix] {
2✔
571
                        for _, c := range l {
2✔
572
                                tokens := strings.Split(c.parsed, " ")
1✔
573
                                if len(tokens) == 6 {
2✔
574
                                        c.parsed = strings.Join(tokens[:5], " ")
1✔
575
                                }
1✔
576
                        }
577
                }
578
        }
579
        stripMetric("route")
1✔
580
        stripMetric("ipv6 route")
1✔
581

1✔
582
        // Normalize lines
1✔
583
        // aaa-server NAME [(interface-name)] host {IP|NAME} [key] [timeout SECONDS]
1✔
584
        // - strip (interface-name), key, timeout
1✔
585
        // - substitute {IP|NAME} by "x"
1✔
586
        // - check that multiple occurrences with same name but different host
1✔
587
        //   all use the same ldap-attribute-map
1✔
588
        // - replace multiple occurrences of this line by one line
1✔
589
        for name, l := range lookup["aaa-server"] {
2✔
590
                ldapMap := " " // invalid name
1✔
591
                if !strings.HasSuffix(l[0].parsed, "protocol ldap") {
2✔
592
                        continue
1✔
593
                }
594
                if len(l) > 1 {
2✔
595
                        for _, c := range l[1:] {
2✔
596
                                words := strings.Split(c.parsed, " ")
1✔
597
                                // Strip (interface-name)
1✔
598
                                if words[2][0] == '(' {
2✔
599
                                        copy(words[2:], words[3:])
1✔
600
                                }
1✔
601
                                if words[2] == "host" {
2✔
602
                                        words[3] = "x"    // Change to value generated by Netspoc.
1✔
603
                                        words = words[:4] // Strip key, timeout
1✔
604
                                        ref := ""
1✔
605
                                        if len(c.sub) != 0 {
2✔
606
                                                ref = c.sub[0].ref[0]
1✔
607
                                        }
1✔
608
                                        if ldapMap != " " && ldapMap != ref {
2✔
609
                                                errlog.Abort("aaa-server %s must not use different values"+
1✔
610
                                                        " in 'ldap-attribute-map'",
1✔
611
                                                        name)
1✔
612
                                        }
1✔
613
                                        ldapMap = ref
1✔
614
                                        c.parsed = strings.Join(words, " ")
1✔
615
                                }
616
                        }
617
                        lookup["aaa-server"][name] = l[0:2]
1✔
618
                }
619
        }
620
        // Normalize subcommand "subject-name *"
621
        // of "crypto ca certificate map" to lowercase for comparison with device,
622
        // because it gets stored in lowercase on device.
623
        for _, l := range lookup["crypto ca certificate map"] {
2✔
624
                for _, c := range l {
2✔
625
                        for _, sc := range c.sub {
2✔
626
                                if strings.HasPrefix(sc.parsed, "subject-name") {
2✔
627
                                        sc.parsed = strings.ToLower(sc.parsed)
1✔
628
                                }
1✔
629
                        }
630
                }
631
        }
632
        // Mark tunnel-group having IP address as name.
633
        for name, l := range lookup["tunnel-group"] {
2✔
634
                if _, err := netip.ParseAddr(name); err == nil {
2✔
635
                        for _, c := range l {
2✔
636
                                c.fixedName = true
1✔
637
                                c.anchor = true
1✔
638
                        }
1✔
639
                }
640
        }
641
}
642

643
var protoNames = map[string]int{
644
        "ah":     51,
645
        "ahp":    51,
646
        "eigrp":  88,
647
        "esp":    50,
648
        "gre":    47,
649
        "igmp":   2,
650
        "igrp":   9,
651
        "ipinip": 4,
652
        "ipsec":  50,
653
        "nos":    94,
654
        "ospf":   89,
655
        "pcp":    108,
656
        "pim":    103,
657
        "pptp":   47,
658
        "sctp":   132,
659
        "snp":    109,
660
}
661

662
var protoNonNumeric = map[string]string{
663
        "1":  "icmp",
664
        "58": "icmp6",
665
        "6":  "tcp",
666
        "17": "udp",
667
}
668
var tcpNames = map[string]int{
669
        "aol":                 5190,
670
        "bgp":                 179,
671
        "chargen":             19,
672
        "cifs":                3020,
673
        "citrix-ica":          1494,
674
        "cmd":                 514,
675
        "connectedapps-plain": 15001,
676
        "connectedapps-tls":   15002,
677
        "ctiqbe":              2748,
678
        "daytime":             13,
679
        "discard":             9,
680
        "domain":              53,
681
        "drip":                3949,
682
        "echo":                7,
683
        "exec":                512,
684
        "finger":              79,
685
        "ftp":                 21,
686
        "ftp-data":            20,
687
        "gopher":              70,
688
        "h323":                1720,
689
        "hostname":            101,
690
        "http":                80,
691
        "https":               443,
692
        "ident":               113,
693
        "imap4":               143,
694
        "irc":                 194,
695
        "kerberos":            750,
696
        "klogin":              543,
697
        "kshell":              544,
698
        "ldap":                389,
699
        "ldaps":               636,
700
        "login":               513,
701
        "lotusnotes":          1352,
702
        "lpd":                 515,
703
        "msrpc":               135,
704
        "netbios-ssn":         139,
705
        "nfs":                 2049,
706
        "nntp":                119,
707
        "onep-plain":          15001,
708
        "onep-tls":            15002,
709
        "pcanywhere-data":     5631,
710
        "pim-auto-rp":         496,
711
        "pop2":                109,
712
        "pop3":                110,
713
        "pptp":                1723,
714
        "rsh":                 514,
715
        "rtsp":                554,
716
        "sip":                 5060,
717
        "smtp":                25,
718
        "sqlnet":              1521,
719
        "ssh":                 22,
720
        "sunrpc":              111,
721
        "tacacs":              49,
722
        "tacacs-ds":           65,
723
        "talk":                517,
724
        "telnet":              23,
725
        "time":                37,
726
        "uucp":                540,
727
        "whois":               43,
728
        "www":                 80,
729
}
730

731
var udpNames = map[string]int{
732
        "biff":              512,
733
        "bootpc":            68,
734
        "bootps":            67,
735
        "cifs":              3020,
736
        "discard":           9,
737
        "dns":               53,
738
        "dnsix":             195,
739
        "domain":            53,
740
        "echo":              7,
741
        "http":              80,
742
        "isakmp":            500,
743
        "kerberos":          750,
744
        "mobile-ip":         434,
745
        "nameserver":        42,
746
        "netbios-dgm":       138,
747
        "netbios-ns":        137,
748
        "netbios-ss":        139,
749
        "nfs":               2049,
750
        "non500-isakmp":     4500,
751
        "ntp":               123,
752
        "pcanywhere-status": 5632,
753
        "pim-auto-rp":       496,
754
        "radius":            1645,
755
        "radius-acct":       1646,
756
        "rip":               520,
757
        "ripng":             521,
758
        "ripv6":             521,
759
        "secureid-udp":      5510,
760
        "sip":               5060,
761
        "snmp":              161,
762
        "snmptrap":          162,
763
        "sunrpc":            111,
764
        "syslog":            514,
765
        "tacacs":            49,
766
        "tacacs-ds":         65,
767
        "talk":              517,
768
        "tftp":              69,
769
        "time":              37,
770
        "vxlan":             4789,
771
        "who":               513,
772
        "www":               80,
773
        "xdmcp":             177,
774
}
775

776
var icmpTypeCodes = map[string]string{
777
        "administratively-prohibited": "3 13",
778
        "alternate-address":           "6",
779
        "conversion-error":            "31",
780
        "dod-host-prohibited":         "3 10",
781
        "dod-net-prohibited":          "3 9",
782
        "echo":                        "8",
783
        "echo-reply":                  "0",
784
        "general-parameter-problem":   "12 0",
785
        "host-isolated":               "3 8",
786
        "host-precedence-unreachable": "3 14",
787
        "host-redirect":               "5 1",
788
        "host-tos-redirect":           "5 3",
789
        "host-tos-unreachable":        "3 12",
790
        "host-unknown":                "3 7",
791
        "host-unreachable":            "3 1",
792
        "information-reply":           "16",
793
        "information-request":         "15",
794
        "mask-reply":                  "18",
795
        "mask-request":                "17",
796
        "mobile-redirect":             "32",
797
        "net-redirect":                "5 0",
798
        "net-tos-redirect":            "5 2",
799
        "net-tos-unreachable":         "3 11",
800
        "net-unreachable":             "3 0",
801
        "network-unknown":             "3 6",
802
        "no-room-for-option":          "12 2",
803
        "option-missing":              "12 1",
804
        "packet-too-big":              "3 4",
805
        "parameter-problem":           "12",
806
        "port-unreachable":            "3 3",
807
        "precedence-unreachable":      "3 15",
808
        "protocol-unreachable":        "3 2",
809
        "reassembly-timeout":          "11",
810
        "redirect":                    "5",
811
        "router-advertisement":        "9",
812
        "router-solicitation":         "10",
813
        "source-quench":               "4",
814
        "source-route-failed":         "3 5",
815
        "time-exceeded":               "11",
816
        "timestamp-reply":             "14",
817
        "timestamp-request":           "13",
818
        "traceroute":                  "30",
819
        "ttl-exceeded":                "11 0",
820
        "unreachable":                 "3",
821
}
822
var icmp6Types = map[string]int{
823
        "echo":                   128,
824
        "echo-reply":             129,
825
        "membership-query":       130,
826
        "membership-reduction":   132,
827
        "membership-report":      131,
828
        "neighbor-advertisement": 136,
829
        "neighbor-redirect":      137,
830
        "neighbor-solicitation":  135,
831
        "packet-too-big":         2,
832
        "parameter-problem":      4,
833
        "router-advertisement":   134,
834
        "router-renumbering":     138,
835
        "router-solicitation":    133,
836
        "time-exceeded":          3,
837
        "unreachable":            1,
838
}
839

840
var logNames = map[string]int{
841
        "emergencies":   0,
842
        "alerts":        1,
843
        "critical":      2,
844
        "errors":        3,
845
        "warnings":      4,
846
        "notifications": 5,
847
        "informational": 6,
848
        "debugging":     7,
849
}
850

851
func postprocessIOSACL(c *cmd) {
1✔
852
        tokens := strings.Fields(c.parsed)
1✔
853
        // Remove sequence number shown since IOS-XE 16.12.
1✔
854
        if tokens[0] == "$SEQ" {
2✔
855
                tokens = tokens[1:]
1✔
856
                c.parsed = strings.Join(tokens, " ")
1✔
857
                _, c.orig, _ = strings.Cut(c.orig, " ")
1✔
858
        }
1✔
859
        if tokens[0] == "remark" {
2✔
860
                return
1✔
861
        }
1✔
862
        // Skip "deny|permit"
863
        parts := tokens[1:]
1✔
864
        // Variables 'tokens' and 'parts' use same backing store.
1✔
865
        postprocessACLParts(c, parts)
1✔
866
        tokens = slices.DeleteFunc(tokens, func(w string) bool { return w == "" })
2✔
867
        c.parsed = strings.Join(tokens, " ")
1✔
868
}
869

870
// Postprocess command
871
// access-list $NAME extended deny|permit PROTO SRC [PORT] DST [PORT]
872
// - Replace named TCP, UDP ports and ICMP type by number
873
// - Replace named log level by number
874
// - Replace reference to object-group by $REF
875
// - Replace network with host mask by host
876
func postprocessASAACL(c *cmd) {
1✔
877
        tokens := strings.Fields(c.parsed)
1✔
878
        if tokens[2] != "extended" {
2✔
879
                return
1✔
880
        }
1✔
881
        // Skip "access-list $NAME extended deny|permit"
882
        parts := tokens[4:]
1✔
883
        // Variables 'tokens' and 'parts' use same backing store.
1✔
884
        postprocessACLParts(c, parts)
1✔
885
        tokens = slices.DeleteFunc(tokens, func(w string) bool { return w == "" })
2✔
886
        c.parsed = strings.Join(tokens, " ")
1✔
887
}
888

889
func postprocessACLParts(c *cmd, parts []string) {
1✔
890
        proto := ""
1✔
891

1✔
892
        convNamed := func(m map[string]int) {
2✔
893
                if len(parts) > 0 {
2✔
894
                        if num, found := m[parts[0]]; found {
2✔
895
                                parts[0] = strconv.Itoa(num)
1✔
896
                        }
1✔
897
                        parts = parts[1:]
1✔
898
                }
899
        }
900
        convNamedPort := func() {
2✔
901
                switch proto {
1✔
902
                case "tcp":
1✔
903
                        convNamed(tcpNames)
1✔
904
                case "udp":
1✔
905
                        convNamed(udpNames)
1✔
906
                }
907
        }
908
        convObjectGroup := func() {
2✔
909
                name := parts[1]
1✔
910
                parts[1] = "$REF"
1✔
911
                c.ref = append(c.ref, name)
1✔
912
                parts = parts[2:]
1✔
913
        }
1✔
914
        convProto := func() {
2✔
915
                switch parts[0] {
1✔
916
                case "object-group":
1✔
917
                        convObjectGroup()
1✔
918
                case "object":
1✔
919
                        parts = parts[2:]
1✔
920
                default:
1✔
921
                        if name, found := protoNonNumeric[parts[0]]; found {
2✔
922
                                parts[0] = name
1✔
923
                        }
1✔
924
                        proto = parts[0]
1✔
925
                        convNamed(protoNames)
1✔
926
                }
927
        }
928
        convObject := func() {
2✔
929
                if len(parts) > 0 {
2✔
930
                        switch parts[0] {
1✔
931
                        case "object-group":
1✔
932
                                convObjectGroup()
1✔
933
                        case "log", "log-input":
1✔
934
                                parts = parts[1:]
1✔
935
                                if len(parts) > 0 {
2✔
936
                                        if num, found := logNames[parts[0]]; found {
2✔
937
                                                parts[0] = strconv.Itoa(num)
1✔
938
                                        }
1✔
939
                                        if parts[0] == "6" {
2✔
940
                                                parts[0] = ""
1✔
941
                                        }
1✔
942
                                        parts = parts[1:]
1✔
943
                                }
944
                                convNamed(logNames)
1✔
945
                        case "host", "object", "object-group-security", "object-group-user",
946
                                "security-group", "user", "user-group":
1✔
947
                                parts = parts[2:]
1✔
948
                        case "any", "any4", "any6", "interface":
1✔
949
                                parts = parts[1:]
1✔
950
                        default:
1✔
951
                                if ip, bits, found := strings.Cut(parts[0], "/"); found {
2✔
952
                                        switch bits {
1✔
953
                                        case "0":
1✔
954
                                                parts[0] = "any6"
1✔
955
                                        case "128":
1✔
956
                                                parts[0] = "host " + ip
1✔
957
                                        }
958
                                        parts = parts[1:]
1✔
959
                                } else if len(parts) >= 2 {
2✔
960
                                        switch parts[1] {
1✔
961
                                        case "0.0.0.0":
1✔
962
                                                parts[0], parts[1] = "any4", ""
1✔
963
                                        case "255.255.255.255":
1✔
964
                                                parts[0], parts[1] = "host", parts[0]
1✔
965
                                        }
966
                                        parts = parts[2:]
1✔
967
                                }
968
                        }
969
                }
970
        }
971
        convPortOrObject := func() {
2✔
972
                if len(parts) > 0 {
2✔
973
                        switch parts[0] {
1✔
974
                        case "eq", "gt", "lt", "neq":
1✔
975
                                parts = parts[1:]
1✔
976
                                convNamedPort()
1✔
977
                        case "range":
1✔
978
                                parts = parts[1:]
1✔
979
                                convNamedPort()
1✔
980
                                convNamedPort()
1✔
981
                        default:
1✔
982
                                convObject()
1✔
983
                        }
984
                }
985
        }
986
        convICMP := func() {
2✔
987
                if len(parts) > 0 {
2✔
988
                        switch proto {
1✔
989
                        case "icmp":
1✔
990
                                if replace, found := icmpTypeCodes[parts[0]]; found {
2✔
991
                                        parts[0] = replace
1✔
992
                                        parts = parts[1:]
1✔
993
                                }
1✔
994
                        case "icmp6":
1✔
995
                                convNamed(icmp6Types)
1✔
996
                        }
997
                }
998
        }
999
        skipNumber := func() {
2✔
1000
                if len(parts) > 0 {
2✔
1001
                        if _, err := strconv.ParseUint(parts[0], 10, 8); err == nil {
2✔
1002
                                parts = parts[1:]
1✔
1003
                        }
1✔
1004
                }
1005
        }
1006

1007
        convProto()
1✔
1008
        convObject()
1✔
1009
        switch proto {
1✔
1010
        case "tcp", "udp":
1✔
1011
                convPortOrObject()
1✔
1012
                convPortOrObject()
1✔
1013
                convPortOrObject()
1✔
1014
        case "icmp", "icmp6":
1✔
1015
                convObject()
1✔
1016
                convICMP()
1✔
1017
                skipNumber()
1✔
1018
        default:
1✔
1019
                convObject()
1✔
1020
        }
1021
        convObject()
1✔
1022
}
1023

1024
// Postprocess subcommands of object-groups:
1025
// - replace named TCP, UDP ports and ICMP type by number
1026
// - replace network with host mask by host
1027
// port-object eq NAME
1028
// port-object range NAME1 NAME2
1029
// icmp-object NAME
1030
// service-object { protocol |{ tcp | udp |tcp-udp | sctp }
1031
//                  [ source operator number ]
1032
//                  [ destination operator number ]|
1033
//                { icmp | icmp6 }[ icmp_type [ icmp_code ]]
1034
//
1035
// NOT IMPLEMENTED, since Netspoc currently not generates object-groups
1036
// of services.
1037

1038
// Remove definitions of banner lines from IOS config.
1039
// banner xxx ^CC
1040
// <lines>
1041
// ^C
1042
func removeBanner(data []byte) []byte {
1✔
1043
        rx := regexp.MustCompile(`^banner\s\S+\s+(.)\S`)
1✔
1044
        i := 0
1✔
1045
        j := 0
1✔
1046
        var endBanner []byte = nil
1✔
1047
        for {
2✔
1048
                e := bytes.Index(data[i:], []byte("\n"))
1✔
1049
                if e == -1 {
2✔
1050
                        j += copy(data[j:], data[i:])
1✔
1051
                        break
1✔
1052
                }
1053
                e++
1✔
1054
                e += i
1✔
1055
                line := data[i:e]
1✔
1056
                i = e
1✔
1057
                if endBanner != nil {
2✔
1058
                        if bytes.HasPrefix(line, endBanner) {
2✔
1059
                                endBanner = nil
1✔
1060
                        }
1✔
1061
                        continue
1✔
1062
                }
1063
                if m := rx.FindSubmatch(line); m != nil {
2✔
1064
                        endBanner = m[1]
1✔
1065
                        continue
1✔
1066
                }
1067
                j += copy(data[j:], line)
1✔
1068
        }
1069
        return data[:j]
1✔
1070
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc