summaryrefslogtreecommitdiff
path: root/src/helpers/strip-private.py
blob: 420a039eb10902bffae3e83b3908dd10e244865b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/usr/bin/python3

# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.  If not, see <http://www.gnu.org/licenses/>.

import argparse
import re
import sys

from netaddr import IPNetwork, AddrFormatError


parser = argparse.ArgumentParser(description='strip off private information from VyOS config')

strictness = parser.add_mutually_exclusive_group()
strictness.add_argument('--loose', action='store_true', help='remove only information specified as arguments')
strictness.add_argument('--strict', action='store_true', help='remove any private information (implies all arguments below). This is the default behavior.')

parser.add_argument('--mac', action='store_true', help='strip off MAC addresses')
parser.add_argument('--hostname', action='store_true', help='strip off system host and domain names')
parser.add_argument('--username', action='store_true', help='strip off user names')
parser.add_argument('--dhcp', action='store_true', help='strip off DHCP shared network and static mapping names')
parser.add_argument('--domain', action='store_true', help='strip off domain names')
parser.add_argument('--asn', action='store_true', help='strip off BGP ASNs')
parser.add_argument('--snmp', action='store_true', help='strip off SNMP location information')
parser.add_argument('--lldp', action='store_true', help='strip off LLDP location information')

address_preserval = parser.add_mutually_exclusive_group()
address_preserval.add_argument('--address', action='store_true', help='strip off all IPv4 and IPv6 addresses')
address_preserval.add_argument('--public-address', action='store_true', help='only strip off public IPv4 and IPv6 addresses')
address_preserval.add_argument('--keep-address', action='store_true', help='preserve all IPv4 and IPv6 addresses')

# Censor the first half of the address.
ipv4_re = re.compile(r'(\d{1,3}\.){2}(\d{1,3}\.\d{1,3})')
ipv4_subst = r'xxx.xxx.\2'

# Censor all but the first two fields.
ipv6_re = re.compile(r'([0-9a-fA-F]{1,4}\:){2}(\S+)')
ipv6_subst = r'xxxx:xxxx:\2'

def ip_match(match: re.Match, subst: str) -> str:
    """
    Take a Match and a substitution pattern, check if the match contains a valid IP address, strip
    information if it is. This routine is intended to be passed to `re.sub' as a replacement pattern.
    """
    result = match.group(0)
    # Is this a valid IP address?
    try:
        addr = IPNetwork(result).ip
    # No? Then we've got nothing to do with it.
    except AddrFormatError:
        return result
    # Should we strip it?
    if args.address or (args.public_address and not addr.is_private()):
        return match.expand(subst)
    # No? Then we'll leave it as is.
    else:
        return result

def strip_address(line: str) -> str:
    """
    Strip IPv4 and IPv6 addresses from the given string.
    """
    return ipv4_re.sub(lambda match: ip_match(match, ipv4_subst), ipv6_re.sub(lambda match: ip_match(match, ipv6_subst), line))

def strip_lines(rules: tuple) -> None:
    """
    Read stdin line by line and apply the given stripping rules.
    """
    try:
        for line in sys.stdin:
            if not args.keep_address:
                line = strip_address(line)
            for (condition, regexp, subst) in rules:
                if condition:
                    line = regexp.sub(subst, line)
            print(line, end='')
    # stdin can be cut for any reason, such as user interrupt or the pager terminating before the text can be read.
    # All we can do is gracefully exit.
    except (BrokenPipeError, EOFError, KeyboardInterrupt):
        sys.exit(1)

if __name__ == "__main__":
    args = parser.parse_args()
    # Strict mode is the default and the absence of loose mode implies presence of strict mode.
    if not args.loose:
        for arg in [args.mac, args.domain, args.hostname, args.username, args.dhcp, args.asn, args.snmp, args.lldp]:
            arg = True
        if not args.public_address and not args.keep_address:
            args.address = True
    elif not args.address and not args.public_address:
        args.keep_address = True
    # (condition, precompiled regexp, substitution string)
    stripping_rules = [
        # Strip passwords
        (True, re.compile(r'password \S+'), 'password xxxxxx'),
        # Strip public key information
        (True, re.compile(r'public-keys \S+'), 'public-keys xxxx@xxx.xxx'),
        (True, re.compile(r'type \'ssh-(rsa|dss)\''), 'type ssh-xxx'),
        (True, re.compile(r' key \S+'), ' key xxxxxx'),
        # Strip OpenVPN secrets
        (True, re.compile(r'(shared-secret-key-file|ca-cert-file|cert-file|dh-file|key-file|client) (\S+)'), r'\1 xxxxxx'),
        # Strip IPSEC secrets
        (True, re.compile(r'pre-shared-secret \S+'), 'pre-shared-secret xxxxxx'),
        # Strip OSPF md5-key
        (True, re.compile(r'md5-key \S+'), 'md5-key xxxxxx'),
        
        # Strip MAC addresses
        (args.mac, re.compile(r'([0-9a-fA-F]{2}\:){5}([0-9a-fA-F]{2}((\:{0,1})){3})'), r'XX:XX:XX:XX:XX:\2'),

        # Strip host-name, domain-name, and domain-search
        (args.hostname, re.compile(r'(host-name|domain-name|domain-search) \S+'), r'\1 xxxxxx'),
        
        # Strip user-names
        (args.username, re.compile(r'(user|username|user-id) \S+'), r'\1 xxxxxx'),
        # Strip full-name
        (args.username, re.compile(r'(full-name) [ -_A-Z a-z]+'), r'\1 xxxxxx'),
        
        # Strip DHCP static-mapping and shared network names
        (args.dhcp, re.compile(r'(shared-network-name|static-mapping) \S+'), r'\1 xxxxxx'),
        
        # Strip host/domain names
        (args.domain, re.compile(r' (peer|remote-host|local-host|server) ([\w-]+\.)+[\w-]+'), r' \1 xxxxx.tld'),
        
        # Strip BGP ASNs
        (args.asn, re.compile(r'(bgp|remote-as) (\d+)'), r'\1 XXXXXX'),
        
        # Strip LLDP location parameters
        (args.lldp, re.compile(r'(altitude|datum|latitude|longitude|ca-value|country-code) (\S+)'), r'\1 xxxxxx'),
        
        # Strip SNMP location
        (args.snmp, re.compile(r'(location) \S+'), r'\1 xxxxxx'),
    ]
    strip_lines(stripping_rules)