#!/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)