├── sanicap ├── __init__.py └── sanicap.py ├── .gitignore ├── requirements.txt ├── Dockerfile └── README.md /sanicap/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pcapng 3 | *.pcap 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ipaddress==1.0.7 2 | pypcapfile==0.12.0 3 | scapy==2.4.5 4 | 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:focal 2 | 3 | RUN apt-get -q update && \ 4 | DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ 5 | python3 \ 6 | python3-pip \ 7 | libpcap0.8-dev && \ 8 | apt-get clean && rm -rf /var/lib/apt/lists/* 9 | 10 | # Install remaining sanipcap deps with pip 11 | COPY requirements.txt / 12 | RUN pip3 install --no-cache-dir -r /requirements.txt && \ 13 | rm /requirements.txt && rm -rf ~/.cache/pip 14 | 15 | # Install app 16 | COPY sanicap/sanicap.py /usr/local/bin/ 17 | RUN chmod +x /usr/local/bin/sanicap.py 18 | ENTRYPOINT ["/usr/bin/python3", "/usr/local/bin/sanicap.py"] 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sanicap 2 | 3 | A python based packet capture sanitizer 4 | 5 | ## Features 6 | * Clean your pcaps for worry-free sharing 7 | * Can randomize/sequentialize: 8 | * MAC addresses 9 | * IPv4/IPv6 addresses 10 | * Override VLAN number 11 | * Integrates with scapy, pyshark, cloud-pcap 12 | 13 | ## Installation 14 | - A `setup.py` file is coming soon. For now, copy to directory of your project. 15 | - to build a docker container, check out this repository and then run `docker build -t sanicap` in the 16 | top level directory. 17 | 18 | ## Usage 19 | ### In another python script 20 | ```python 21 | >>> from sanicap import sanitize 22 | >>> sanicap.sanitize('/path/to/test.pcap', sequential=True, ipv4_mask=8, ipv6_mask=16) 23 | This file has 23 IPv4/IPv6 endpoints and 6 MAC endpoints 24 | File created: /path/to/test_sanitized_141205-124237.pcap 25 | ``` 26 | 27 | ### As a CLI utility 28 | The examples below use the docker container, but this would also work if the dependencies in `requirements.txt` are 29 | installed directly on your system. 30 | #### Help: 31 | ```console 32 | $ docker run -ti sanicap -h 33 | usage: sanicap.py [-h] [-o FILEPATH_OUT] [-s SEQUENTIAL] [-a APPEND] [--ipv4mask IPV4MASK] [--ipv6mask IPV6MASK] 34 | [--macmask MACMASK] [--startipv4 STARTIPV4] [--startipv6 STARTIPV6] [--startmac STARTMAC] 35 | [--fixedvlan FIXEDVLAN] 36 | filepath_in 37 | 38 | positional arguments: 39 | filepath_in The pcap file to sanitize. 40 | 41 | optional arguments: 42 | -h, --help show this help message and exit 43 | -o FILEPATH_OUT, --filepath_out FILEPATH_OUT 44 | File path to store the sanitized pcap. 45 | -s SEQUENTIAL, --sequential SEQUENTIAL 46 | Use sequential IPs/MACs in sanitization. 47 | -a APPEND, --append APPEND 48 | Append to, instead of overwriting output file.. 49 | --ipv4mask IPV4MASK Apply a mask to sanitized IPv4 addresses (Eg. mask of 8 preserves first octet). 50 | --ipv6mask IPV6MASK Apply a mask to sanitized IPv6 addresses (Eg. mask of 16 preserves first chazwazza). 51 | --macmask MACMASK Apply a mask to sanitized IPv6 addresses (Eg. mask of 24 preserves manufacturer). 52 | --startipv4 STARTIPV4 53 | Start sequential IPv4 sanitization with this IPv4 addresses. 54 | --startipv6 STARTIPV6 55 | Start sequential IPv6 sanitization with this IPv6 addresses. 56 | --startmac STARTMAC Start sequential MAC sanitization with this MAC addresses. 57 | --fixedvlan FIXEDVLAN 58 | Overwrite VLANID (fixed) 59 | 60 | usage: sanicap.py [-h] [-o FILEPATH_OUT] [-s SEQUENTIAL] [--ipv4mask IPV4MASK] 61 | [--ipv6mask IPV6MASK] [--macmask MACMASK] 62 | [--startipv4 STARTIPV4] [--startipv6 STARTIPV6] 63 | [--startmac STARTMAC] 64 | filepath_in 65 | ``` 66 | #### Example: 67 | ```console 68 | $ docker run -ti -v $(pwd):/data sanicap /data/test.pcap -o /data/out.pcap -s True --ipv4mask=8 69 | This file has 85 IPv4/IPv6 endpoints and 38 MAC endpoints 70 | File created: /data/out.pcap 71 | ``` 72 | 73 | ## ToDo 74 | * Add pcapng support 75 | * standalone CLI usage 76 | * Anonymize DNS Queries 77 | * Anonymize HTTP host info 78 | * Anonymize HTTP data? (not sure what yet, maybe just POST data) 79 | * python BPF capture filter (apply to pcap files) 80 | -------------------------------------------------------------------------------- /sanicap/sanicap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import ipaddress 5 | import os 6 | import textwrap 7 | from random import randint 8 | 9 | from pcapfile import savefile 10 | from scapy.all import Ether 11 | from scapy.utils import PcapWriter 12 | 13 | 14 | class MACGenerator(object): 15 | def __init__(self, start_mac, sequential, mask): 16 | self.start_mac = self._last_mac = start_mac 17 | self.started = False 18 | self.mappings = {} 19 | self.sequential = sequential 20 | self.mask = mask 21 | 22 | def _increment(self, address): 23 | 24 | # pad hex number first so it's the correct length 25 | def pad_bin(unpadded): 26 | return format(int('0x' + unpadded.replace(':', '').replace('.', ''), 16), '048b') 27 | 28 | mac_bin = pad_bin(self._last_mac) 29 | 30 | # check to make sure we haven't hit highest number in mask (and wrapped back to 0) 31 | if '0' not in mac_bin[self.mask:]: 32 | raise OverflowError('Ran out of MAC addresses, try a smaller mask or lower starting MAC.') 33 | 34 | # only increment if it's not the first iteration 35 | if self.started: 36 | if self.mask > 0: 37 | masked = format(int(pad_bin(address)[:self.mask], 2), '0' + str(self.mask) + 'b') 38 | unmasked = format(int(mac_bin[self.mask:], 2) + 1, '0' + str(48 - self.mask) + 'b') 39 | returned_bin = format(int(masked + unmasked, 2), '012x') 40 | else: 41 | returned_bin = format(int(mac_bin, 2) + 1, '012x') 42 | 43 | else: 44 | self.started = True 45 | if self.mask > 0: 46 | masked = format(int(pad_bin(address)[:self.mask], 2), '0%sb' % str(self.mask)) 47 | unmasked = format(int(mac_bin[self.mask:], 2), '0%sb' % str(48 - self.mask)) 48 | returned_bin = format(int(masked + unmasked, 2), '012x') 49 | else: 50 | returned_bin = format(int(mac_bin, 2), '012x') 51 | 52 | return ':'.join(textwrap.wrap(returned_bin, 2)) 53 | 54 | def _random_mac(self, address): 55 | 56 | def pad_bin(unpadded): 57 | return format(int('0x' + unpadded.replace(':', '').replace('.', ''), 16), '048b') 58 | 59 | unmasked = ''.join(str(randint(0, 1)) for x in range(0, 48 - self.mask)) 60 | 61 | full_bin = pad_bin(address)[:self.mask] + unmasked 62 | 63 | return ':'.join(textwrap.wrap(format(int(full_bin, 2), '012x'), 2)) 64 | 65 | def _next_mac(self, address): 66 | 67 | if self.sequential: 68 | self._last_mac = self._increment(address) 69 | else: 70 | self._last_mac = self._random_mac(address) 71 | 72 | if self._last_mac not in iter(self.mappings.values()): 73 | return self._last_mac 74 | else: 75 | return self._next_mac(address) 76 | 77 | def get_mac(self, address): 78 | # check address mapping 79 | generic_addresses = ['ff:ff:ff:ff:ff:ff', '00:00:00:00:00:00'] 80 | if address not in generic_addresses: 81 | try: 82 | return self.mappings[address] 83 | except KeyError: 84 | self.mappings[address] = self._next_mac(address) 85 | return self.mappings[address] 86 | else: 87 | return address 88 | 89 | 90 | class IPv4Generator(object): 91 | def __init__(self, start_ip, sequential, mask): 92 | self.start_ip = self._last_ip = start_ip 93 | self.started = False 94 | self.mappings = {} 95 | self.sequential = sequential 96 | self.mask = mask 97 | 98 | def _increment(self, address): 99 | 100 | # pad binary number first so it's the correct length 101 | def pad_bin(unpadded): 102 | return format(int(ipaddress.IPv4Address(str(unpadded))), '032b') 103 | 104 | ip_bin = pad_bin(self._last_ip) 105 | 106 | # check to make sure we haven't hit highest number in mask (and wrapped back to 0) 107 | if '0' not in ip_bin[self.mask:]: 108 | raise OverflowError('Ran out of IP addresses, try a smaller mask or lower starting IP.') 109 | 110 | # only increment if it's not the first iteration 111 | if self.started: 112 | full_bin = pad_bin(address)[:self.mask] + format(int(ip_bin[self.mask:], 2) + 1, 113 | '0' + str(32 - self.mask) + 'b') 114 | else: 115 | self.started = True 116 | full_bin = pad_bin(address)[:self.mask] + format(int(ip_bin[self.mask:], 2), 117 | '0' + str(32 - self.mask) + 'b') 118 | 119 | return str(ipaddress.IPv4Address(int(full_bin, 2))) 120 | 121 | def _random_ip(self, address): 122 | 123 | def pad_bin(unpadded): 124 | return format(int(ipaddress.IPv4Address(str(unpadded))), '032b') 125 | 126 | unmasked = ''.join(str(randint(0, 1)) for _ in range(0, 32 - self.mask)) 127 | 128 | if self.started: 129 | full_bin = pad_bin(address)[:self.mask] + unmasked 130 | else: 131 | self.started = True 132 | full_bin = pad_bin(address)[:self.mask] + unmasked 133 | 134 | return str(ipaddress.IPv4Address(int(full_bin, 2))) 135 | 136 | def _next_ip(self, address): 137 | 138 | if self.sequential: 139 | self._last_ip = self._increment(address) 140 | else: 141 | self._last_ip = self._random_ip(address) 142 | 143 | if self._last_ip not in iter(self.mappings.values()): 144 | return self._last_ip 145 | else: 146 | return self._next_ip(address) 147 | 148 | def get_ip(self, address): 149 | # check address mapping 150 | generic_addresses = ['255.255.255.255', '0.0.0.0'] 151 | if address not in generic_addresses: 152 | try: 153 | return self.mappings[address] 154 | except KeyError: 155 | self.mappings[address] = self._next_ip(address) 156 | return self.mappings[address] 157 | else: 158 | return address 159 | 160 | 161 | class IPv6Generator(object): 162 | def __init__(self, start_ip, sequential, mask): 163 | self.start_ip = self._last_ip = start_ip 164 | self.started = False 165 | self.mappings = {} 166 | self.sequential = sequential 167 | self.mask = mask 168 | 169 | def _increment(self, address): 170 | 171 | # pad binary number first so it's the correct length 172 | def pad_bin(unpadded): 173 | return format(int(ipaddress.IPv6Address(str(unpadded))), '0128b') 174 | 175 | ip_bin = pad_bin(self._last_ip) 176 | 177 | # check to make sure we haven't hit highest number in mask (and wrapped back to 0) 178 | if '0' not in ip_bin[self.mask:]: 179 | raise OverflowError('Ran out of IP addresses, try a smaller mask or lower starting IP.') 180 | 181 | # only increment if it's not the first iteration 182 | if self.started: 183 | full_bin = pad_bin(address)[:self.mask] + format(int(ip_bin[self.mask:], 2) + 1, 184 | '0' + str(128 - self.mask) + 'b') 185 | else: 186 | self.started = True 187 | full_bin = pad_bin(address)[:self.mask] + format(int(ip_bin[self.mask:], 2), 188 | '0' + str(128 - self.mask) + 'b') 189 | 190 | return str(ipaddress.IPv6Address(int(full_bin, 2))) 191 | 192 | def _random_ip(self, address): 193 | 194 | def pad_bin(unpadded): 195 | return format(int(ipaddress.IPv6Address(str(unpadded))), '0128b') 196 | 197 | unmasked = ''.join(str(randint(0, 1)) for _ in range(0, 128 - self.mask)) 198 | 199 | if self.started: 200 | full_bin = pad_bin(address)[:self.mask] + unmasked 201 | else: 202 | self.started = True 203 | full_bin = pad_bin(address)[:self.mask] + unmasked 204 | 205 | return str(ipaddress.IPv6Address(int(full_bin, 2))) 206 | 207 | def _next_ip(self, address): 208 | 209 | if self.sequential: 210 | self._last_ip = self._increment(address) 211 | else: 212 | self._last_ip = self._random_ip(address) 213 | 214 | if self._last_ip not in iter(self.mappings.values()): 215 | return self._last_ip 216 | else: 217 | return self._next_ip(address) 218 | 219 | def get_ip(self, address): 220 | # check address mapping 221 | if not address.startswith("ff02"): 222 | try: 223 | return self.mappings[address] 224 | except KeyError: 225 | self.mappings[address] = self._next_ip(address) 226 | return self.mappings[address] 227 | else: 228 | return address 229 | 230 | 231 | def sanitize(filepath_in, *, filepath_out=None, sequential=True, append=False, ipv4_mask=0, ipv6_mask=0, mac_mask=0, 232 | start_ipv4='10.0.0.1', start_ipv6='2001:aa::1', start_mac='00:aa:00:00:00:01', fixed_vlan=None, info=True): 233 | if not filepath_out: 234 | timestamp = datetime.datetime.now().strftime('%y%m%d-%H%m%S') 235 | filepath_out = os.path.splitext(filepath_in)[0] + '_sanitized_' + timestamp + os.path.splitext(filepath_in)[1] 236 | 237 | mac_gen = MACGenerator(sequential=sequential, mask=mac_mask, start_mac=start_mac) 238 | ip4_gen = IPv4Generator(sequential=sequential, mask=ipv4_mask, start_ip=start_ipv4) 239 | ip6_gen = IPv6Generator(sequential=sequential, mask=ipv6_mask, start_ip=start_ipv6) 240 | 241 | with open(filepath_in, 'rb') as capfile: 242 | 243 | # open cap file with pcapfile 244 | cap = savefile.load_savefile(capfile, verbose=False) 245 | 246 | # use scapy's pcapwriter 247 | pktwriter = PcapWriter(filepath_out, append=append) 248 | 249 | try: 250 | for pkt in cap.packets: 251 | 252 | # create scapy packet from pcapfile packet raw output 253 | new_pkt = Ether(pkt.raw()) 254 | 255 | # MAC addresses 256 | try: 257 | new_pkt.src = mac_gen.get_mac(new_pkt.src) 258 | new_pkt.dst = mac_gen.get_mac(new_pkt.dst) 259 | except: 260 | pass 261 | 262 | # VLAN number 263 | if fixed_vlan: # not None 264 | try: 265 | new_pkt['Dot1Q'].vlan = fixed_vlan 266 | except: 267 | pass 268 | 269 | # IP Addresses 270 | try: 271 | new_pkt['IP'].src = ip4_gen.get_ip(new_pkt['IP'].src) 272 | new_pkt['IP'].dst = ip4_gen.get_ip(new_pkt['IP'].dst) 273 | except IndexError: 274 | pass 275 | try: 276 | new_pkt['IPv6'].src = ip6_gen.get_ip(new_pkt['IPv6'].src) 277 | new_pkt['IPv6'].dst = ip6_gen.get_ip(new_pkt['IPv6'].dst) 278 | except IndexError: 279 | pass 280 | 281 | # sanitize ARP addresses 282 | try: 283 | new_pkt['ARP'].hwsrc = mac_gen.get_mac(new_pkt['ARP'].hwsrc) 284 | new_pkt['ARP'].hwdst = mac_gen.get_mac(new_pkt['ARP'].hwdst) 285 | new_pkt['ARP'].psrc = ip4_gen.get_ip(new_pkt['ARP'].psrc) 286 | new_pkt['ARP'].pdst = ip4_gen.get_ip(new_pkt['ARP'].pdst) 287 | except IndexError: 288 | pass 289 | 290 | # fix checksum in each layer, starting at the top layer 291 | for layer in range(12, 0, -1): 292 | try: 293 | del new_pkt[layer].chksum 294 | except: 295 | pass 296 | 297 | pktwriter.write(new_pkt) 298 | 299 | finally: 300 | pktwriter.close() 301 | 302 | if info: 303 | print('This file has %s IPv4/IPv6 endpoints and %s MAC endpoints' % ( 304 | len(ip4_gen.mappings) + len(ip6_gen.mappings), len(mac_gen.mappings))) 305 | print('File created: %s' % filepath_out) 306 | 307 | 308 | # If run as a CLI util 309 | if __name__ == '__main__': 310 | import sys 311 | import argparse 312 | 313 | parser = argparse.ArgumentParser() 314 | parser.add_argument("filepath_in", help="The pcap file to sanitize.") 315 | parser.add_argument("-o", "--filepath_out", default=None, help="File path to store the sanitized pcap.") 316 | parser.add_argument("-s", "--sequential", default=True, type=bool, help="Use sequential IPs/MACs in sanitization.") 317 | parser.add_argument("-a", "--append", default=False, type=bool, 318 | help="Append to, instead of overwriting output file..") 319 | parser.add_argument("--ipv4mask", default=0, type=int, 320 | help="Apply a mask to sanitized IPv4 addresses (Eg. mask of 8 preserves first octet).") 321 | parser.add_argument("--ipv6mask", default=0, type=int, 322 | help="Apply a mask to sanitized IPv6 addresses (Eg. mask of 16 preserves first chazwazza).") 323 | parser.add_argument("--macmask", default=0, type=int, 324 | help="Apply a mask to sanitized IPv6 addresses (Eg. mask of 24 preserves manufacturer).") 325 | parser.add_argument("--startipv4", default='10.0.0.1', 326 | help="Start sequential IPv4 sanitization with this IPv4 addresses.") 327 | parser.add_argument("--startipv6", default='2001:aa::1', 328 | help="Start sequential IPv6 sanitization with this IPv6 addresses.") 329 | parser.add_argument("--startmac", default='00:aa:00:00:00:01', 330 | help="Start sequential MAC sanitization with this MAC addresses.") 331 | parser.add_argument("--fixedvlan", default=None, type=int, help="Overwrite VLANID (fixed)") 332 | 333 | args = parser.parse_args() 334 | 335 | try: 336 | sanitize(args.filepath_in, filepath_out=args.filepath_out, sequential=args.sequential, append=args.append, 337 | ipv4_mask=args.ipv4mask, ipv6_mask=args.ipv6mask, mac_mask=args.macmask, start_ipv4=args.startipv4, 338 | start_ipv6=args.startipv6, start_mac=args.startmac, fixed_vlan=args.fixedvlan) 339 | except Exception as e: 340 | if str(e) == "No supported Magic Number found": 341 | print("Error: pcapng is not supported, convert the input to classic pcap (try editcap -F pcap).\n") 342 | else: 343 | print(f"Error: {e}\n") 344 | 345 | parser.print_help() 346 | sys.exit(1) 347 | --------------------------------------------------------------------------------