|
| 1 | +#!/usr/bin/python3 |
| 2 | +# reads the main app data from multiple csv files contained in a git repo |
| 3 | +# users will reside in external ldap groups with standardized names |
| 4 | +# only the main responsible person per app is taken from the csv files |
| 5 | +# this does not use Tufin RLM any longer as a source |
| 6 | +# here app servers will only have ip addresses (no names) |
| 7 | + |
| 8 | +# dependencies: |
| 9 | +# a) package python3-git must be installed |
| 10 | +# b) requires the following config items in /usr/local/orch/etc/secrets/customizingConfig.json (or given config file): |
| 11 | + |
| 12 | +''' |
| 13 | +sample config file /usr/local/orch/etc/secrets/customizingConfig.json |
| 14 | +
|
| 15 | +{ |
| 16 | + "gitRepoUrl": "github.domain.de/CMDB-export", |
| 17 | + "gitusername": "gituser1", |
| 18 | + "gitpassword": "xxx", |
| 19 | + "csvAllOwnerFiles": ["all-apps.csv", "all-infra-services.csv"], |
| 20 | + "csvAppServerFiles": ["app-servers.csv", "com-servers.csv"], |
| 21 | + "ldapPath": "CN={USERID},OU=Benutzer,DC=DOMAIN,DC=DE" |
| 22 | +} |
| 23 | +''' |
| 24 | + |
| 25 | +from asyncio.log import logger |
| 26 | +import traceback |
| 27 | +import requests.packages |
| 28 | +import requests |
| 29 | +import json |
| 30 | +import sys |
| 31 | +import argparse |
| 32 | +import logging |
| 33 | +import os |
| 34 | +from pathlib import Path |
| 35 | +import git # apt install python3-git # or: pip install git |
| 36 | +import csv |
| 37 | +import re |
| 38 | +from netaddr import IPAddress, IPNetwork |
| 39 | + |
| 40 | + |
| 41 | +baseDir = "/usr/local/fworch/" |
| 42 | +baseDirEtc = baseDir + "etc/" |
| 43 | +repoTargetDir = baseDirEtc + "cmdb-repo" |
| 44 | +defaultConfigFileName = baseDirEtc + "secrets/customizingConfig.json" |
| 45 | +importSourceString = "tufinRlm" |
| 46 | + |
| 47 | + |
| 48 | +class Owner: |
| 49 | + def __init__(self, name, app_id_external, main_user, recert_period_days, import_source): |
| 50 | + self.name = name |
| 51 | + self.app_id_external = app_id_external |
| 52 | + self.main_user = main_user |
| 53 | + self.modellers = [] |
| 54 | + self.import_source = import_source |
| 55 | + self.recert_period_days = recert_period_days |
| 56 | + self.app_servers = [] |
| 57 | + |
| 58 | + def to_json(self): |
| 59 | + return ( |
| 60 | + { |
| 61 | + "name": self.name, |
| 62 | + "app_id_external": self.app_id_external, |
| 63 | + "main_user": self.main_user, |
| 64 | + # "criticality": self.criticality, |
| 65 | + "import_source": self.import_source, |
| 66 | + "recert_period_days": self.recert_period_days, |
| 67 | + "app_servers": [ip.to_json() for ip in self.app_servers] |
| 68 | + } |
| 69 | + ) |
| 70 | + |
| 71 | + |
| 72 | +class app_ip: |
| 73 | + def __init__(self, app_id_external: str, ip_start: IPAddress, ip_end: IPAddress, type: str, name: str): |
| 74 | + self.name = name |
| 75 | + self.app_id_external = app_id_external |
| 76 | + self.ip_start = ip_start |
| 77 | + self.ip_end = ip_end |
| 78 | + self.type = type |
| 79 | + |
| 80 | + def to_json(self): |
| 81 | + return ( |
| 82 | + { |
| 83 | + "name": self.name, |
| 84 | + "app_id_external": self.app_id_external, |
| 85 | + "ip_start": str(IPAddress(self.ip_start)), |
| 86 | + "ip_end": str(IPAddress(self.ip_end)), |
| 87 | + "type": self.type |
| 88 | + } |
| 89 | + ) |
| 90 | + |
| 91 | + |
| 92 | +def read_custom_config(configFilename, keyToGet): |
| 93 | + try: |
| 94 | + with open(configFilename, "r") as customConfigFH: |
| 95 | + customConfig = json.loads(customConfigFH.read()) |
| 96 | + return customConfig[keyToGet] |
| 97 | + |
| 98 | + except Exception: |
| 99 | + logger.error("could not read key '" + keyToGet + "' from config file " + configFilename + ", Exception: " + str(traceback.format_exc())) |
| 100 | + sys.exit(1) |
| 101 | + |
| 102 | + |
| 103 | +def build_dn(userId, ldapPath): |
| 104 | + dn = "" |
| 105 | + if len(userId)>0: |
| 106 | + if '{USERID}' in ldapPath: |
| 107 | + dn = ldapPath.replace('{USERID}', userId) |
| 108 | + else: |
| 109 | + logger.error("could not find {USERID} parameter in ldapPath " + ldapPath) |
| 110 | + return dn |
| 111 | + |
| 112 | + |
| 113 | +def get_logger(debug_level_in=0): |
| 114 | + debug_level=int(debug_level_in) |
| 115 | + if debug_level>=1: |
| 116 | + llevel = logging.DEBUG |
| 117 | + else: |
| 118 | + llevel = logging.INFO |
| 119 | + |
| 120 | + logger = logging.getLogger('import-fworch-app-data') |
| 121 | + logformat = "%(asctime)s [%(levelname)-5.5s] [%(filename)-10.10s:%(funcName)-10.10s:%(lineno)4d] %(message)s" |
| 122 | + logging.basicConfig(format=logformat, datefmt="%Y-%m-%dT%H:%M:%S%z", level=llevel) |
| 123 | + logger.setLevel(llevel) |
| 124 | + |
| 125 | + #set log level for noisy requests/connectionpool module to WARNING: |
| 126 | + connection_log = logging.getLogger("urllib3.connectionpool") |
| 127 | + connection_log.setLevel(logging.WARNING) |
| 128 | + connection_log.propagate = True |
| 129 | + |
| 130 | + if debug_level>8: |
| 131 | + logger.debug ("debug_level=" + str(debug_level) ) |
| 132 | + return logger |
| 133 | + |
| 134 | + |
| 135 | + |
| 136 | +def read_app_data_from_csv(csvFile: str): |
| 137 | + try: |
| 138 | + with open(csvFile, newline='') as csvFile: |
| 139 | + reader = csv.reader(csvFile) |
| 140 | + headers = next(reader) # Get header row first |
| 141 | + |
| 142 | + # Define regex patterns for column headers |
| 143 | + name_pattern = re.compile(r'.*?:\s*Name') |
| 144 | + app_id_pattern = re.compile(r'.*?:\s*Alfabet-ID$') |
| 145 | + owner_tiso_pattern = re.compile(r'.*?:\s*TISO') |
| 146 | + owner_kwita_pattern = re.compile(r'.*?:\s*kwITA') |
| 147 | + |
| 148 | + # Find column indices using regex |
| 149 | + app_name_column = next(i for i, h in enumerate(headers) if name_pattern.match(h)) |
| 150 | + app_id_column = next(i for i, h in enumerate(headers) if app_id_pattern.match(h)) |
| 151 | + app_owner_tiso_column = next(i for i, h in enumerate(headers) if owner_tiso_pattern.match(h)) |
| 152 | + app_owner_kwita_column = next(i for i, h in enumerate(headers) if owner_kwita_pattern.match(h)) |
| 153 | + |
| 154 | + apps_from_csv = list(reader) # Read remaining rows |
| 155 | + except Exception: |
| 156 | + logger.error("error while trying to read csv file '" + csvFile + "', exception: " + str(traceback.format_exc())) |
| 157 | + sys.exit(1) |
| 158 | + |
| 159 | + return apps_from_csv, app_name_column, app_id_column, app_owner_tiso_column, app_owner_kwita_column |
| 160 | + |
| 161 | + |
| 162 | +# adds data from csv file to appData |
| 163 | +# order of files in important: we only import apps which are included in files 3 and 4 (which only contain active apps) |
| 164 | +# so first import files 3 and 4, then import files 1 and 2^ |
| 165 | +def extract_app_data_from_csv (csvFile: str, app_list: list): |
| 166 | + |
| 167 | + apps_from_csv = [] |
| 168 | + csvFile = repoTargetDir + '/' + csvFile # add directory to csv files |
| 169 | + |
| 170 | + apps_from_csv, app_name_column, app_id_column, app_owner_tiso_column, app_owner_kwita_column = read_app_data_from_csv(csvFile) |
| 171 | + |
| 172 | + countSkips = 0 |
| 173 | + # append all owners from CSV |
| 174 | + for line in apps_from_csv: |
| 175 | + app_id = line[app_id_column] |
| 176 | + if app_id.lower().startswith('app-') or app_id.lower().startswith('com-'): |
| 177 | + app_name = line[app_name_column] |
| 178 | + app_main_user = line[app_owner_tiso_column] |
| 179 | + main_user_dn = build_dn(app_main_user, ldapPath) |
| 180 | + kwita = line[app_owner_kwita_column] |
| 181 | + if kwita is None or kwita == '' or kwita.lower() == 'nein': |
| 182 | + recert_period_days = 365 |
| 183 | + else: |
| 184 | + recert_period_days = 182 |
| 185 | + if main_user_dn=='': |
| 186 | + logger.warning('adding app without main user: ' + app_id) |
| 187 | + app_list.append(Owner(app_id_external=app_id, name=app_name, main_user=main_user_dn, recert_period_days = recert_period_days, import_source=importSourceString)) |
| 188 | + else: |
| 189 | + logger.info(f'ignoring line from csv file: {app_id} - inconclusive appId') |
| 190 | + countSkips += 1 |
| 191 | + logger.info(f"{str(csvFile)}: #total lines {str(len(apps_from_csv))}, skipped: {str(countSkips)}") |
| 192 | + |
| 193 | + |
| 194 | +def read_ip_data_from_csv(csv_filename): |
| 195 | + try: |
| 196 | + with open(csv_filename, newline='', encoding='utf-8') as csvFile: |
| 197 | + reader = csv.reader(csvFile) |
| 198 | + headers = next(reader) # Get header row first |
| 199 | + |
| 200 | + # Define regex patterns for column headers |
| 201 | + app_id_pattern = re.compile(r'.*?:\s*Alfabet-ID$') |
| 202 | + ip_pattern = re.compile(r'.*?:\s*IP') |
| 203 | + |
| 204 | + # Find column indices using regex |
| 205 | + app_id_column_no = next(i for i, h in enumerate(headers) if app_id_pattern.match(h)) |
| 206 | + ip_column_no = next(i for i, h in enumerate(headers) if ip_pattern.match(h)) |
| 207 | + |
| 208 | + ip_data = list(reader) # Read remaining rows |
| 209 | + except Exception: |
| 210 | + logger.error("error while trying to read csv file '" + csv_filename + "', exception: " + str(traceback.format_exc())) |
| 211 | + sys.exit(1) |
| 212 | + |
| 213 | + return ip_data, app_id_column_no, ip_column_no |
| 214 | + |
| 215 | + |
| 216 | +def parse_ip(line, app_id, ip_column_no, app_dict, count_skips): |
| 217 | + # add app server ip addresses (but do not add the whole app - it must already exist) |
| 218 | + app_server_ip_str = line[ip_column_no] |
| 219 | + if app_server_ip_str is not None and app_server_ip_str != "": |
| 220 | + try: |
| 221 | + ip_range = IPNetwork(app_server_ip_str) |
| 222 | + except Exception: |
| 223 | + logger.warning(f'error parsing IP/network {app_server_ip_str} for app {app_id}, skipping this entry') |
| 224 | + count_skips += 1 |
| 225 | + return count_skips |
| 226 | + if ip_range.size > 1: |
| 227 | + ip_type = "network" |
| 228 | + else: |
| 229 | + ip_type = "host" |
| 230 | + |
| 231 | + app_server_ip = app_ip(app_id_external=app_id, ip_start=ip_range.first, ip_end=ip_range.last, type=ip_type, name=f"{ip_type}_{app_server_ip_str}") |
| 232 | + if app_server_ip not in app_dict[app_id].app_servers: |
| 233 | + app_dict[app_id].app_servers.append(app_server_ip) |
| 234 | + else: |
| 235 | + count_skips += 1 |
| 236 | + |
| 237 | + return count_skips |
| 238 | + |
| 239 | + |
| 240 | +# adds ip data from csv file to appData |
| 241 | +def extract_ip_data_from_csv (csv_filename: str, app_dict: dict[str: Owner]): |
| 242 | + |
| 243 | + valid_app_id_prefixes = ['app-', 'com-'] |
| 244 | + |
| 245 | + ip_data = [] |
| 246 | + csv_filename = repoTargetDir + '/' + csv_filename # add directory to csv files |
| 247 | + |
| 248 | + ip_data, app_id_column_no, ip_column_no = read_ip_data_from_csv(csv_filename) |
| 249 | + |
| 250 | + count_skips = 0 |
| 251 | + # append all owners from CSV |
| 252 | + for line in ip_data: |
| 253 | + app_id: str = line[app_id_column_no] |
| 254 | + app_id_prefix = app_id.split('-')[0].lower() + '-' |
| 255 | + |
| 256 | + if len(valid_app_id_prefixes)==0 or app_id_prefix in valid_app_id_prefixes: |
| 257 | + if app_id in app_dict.keys(): |
| 258 | + count_skips = parse_ip(line, app_id, ip_column_no, app_dict, count_skips) |
| 259 | + else: |
| 260 | + logger.debug(f'ignoring line from csv file: {app_id} - inactive?') |
| 261 | + count_skips += 1 |
| 262 | + else: |
| 263 | + logger.info(f'ignoring line from csv file: {app_id} - inconclusive appId') |
| 264 | + count_skips += 1 |
| 265 | + logger.info(f"{str(csv_filename)}: #total lines {str(len(ip_data))}, skipped: {str(count_skips)}") |
| 266 | + |
| 267 | + |
| 268 | +def transform_owner_dict_to_list(app_data): |
| 269 | + owner_data = { "owners": [] } |
| 270 | + for app_id in app_data: |
| 271 | + owner_data['owners'].append( app_data[app_id].to_json()) |
| 272 | + return owner_data |
| 273 | + |
| 274 | + |
| 275 | +def transform_app_list_to_dict(app_list): |
| 276 | + app_data_dict = {} |
| 277 | + for app in app_list: |
| 278 | + app_data_dict[app.app_id_external] = app |
| 279 | + return app_data_dict |
| 280 | + |
| 281 | + |
| 282 | +if __name__ == "__main__": |
| 283 | + parser = argparse.ArgumentParser( |
| 284 | + description='Read configuration from FW management via API calls') |
| 285 | + parser.add_argument('-c', '--config', default=defaultConfigFileName, |
| 286 | + help='Filename of custom config file for modelling imports') |
| 287 | + parser.add_argument('-s', "--suppress_certificate_warnings", action='store_true', default = True, |
| 288 | + help = "suppress certificate warnings") |
| 289 | + parser.add_argument('-f', "--import_from_folder", |
| 290 | + help = "if set, will try to read csv files from given folder instead of git repo") |
| 291 | + parser.add_argument('-l', '--limit', metavar='api_limit', default='150', |
| 292 | + help='The maximal number of returned results per HTTPS Connection; default=50') |
| 293 | + |
| 294 | + args = parser.parse_args() |
| 295 | + |
| 296 | + if args.suppress_certificate_warnings: |
| 297 | + requests.packages.urllib3.disable_warnings() |
| 298 | + |
| 299 | + logger = get_logger(debug_level_in=2) |
| 300 | + |
| 301 | + # read config |
| 302 | + ldapPath = read_custom_config(args.config, 'ldapPath') |
| 303 | + gitRepoUrl = read_custom_config(args.config, 'gitRepo') |
| 304 | + gitUsername = read_custom_config(args.config, 'gitUser') |
| 305 | + gitPassword = read_custom_config(args.config, 'gitpassword') |
| 306 | + csvAllOwnerFiles = read_custom_config(args.config, 'csvAllOwnerFiles') |
| 307 | + csvAppServerFiles = read_custom_config(args.config, 'csvAppServerFiles') |
| 308 | + |
| 309 | + ############################################# |
| 310 | + # 1. get CSV files from github repo |
| 311 | + |
| 312 | + try: |
| 313 | + repoUrl = "https://" + gitUsername + ":" + gitPassword + "@" + gitRepoUrl |
| 314 | + if os.path.exists(repoTargetDir): |
| 315 | + # If the repository already exists, open it and perform a pull |
| 316 | + repo = git.Repo(repoTargetDir) |
| 317 | + origin = repo.remotes.origin |
| 318 | + origin.pull() |
| 319 | + else: |
| 320 | + repo = git.Repo.clone_from(repoUrl, repoTargetDir) |
| 321 | + except Exception as e: |
| 322 | + logger.warning("could not clone/pull git repo from " + repoUrl + ", exception: " + str(traceback.format_exc())) |
| 323 | + logger.warning("trying to read csv files from folder given as parameter...") |
| 324 | + # sys.exit(1) |
| 325 | + |
| 326 | + ############################################# |
| 327 | + # 2. get app data from CSV files |
| 328 | + app_list = [] |
| 329 | + for csvFile in csvAllOwnerFiles: |
| 330 | + extract_app_data_from_csv(csvFile, app_list) |
| 331 | + |
| 332 | + app_dict = transform_app_list_to_dict(app_list) |
| 333 | + |
| 334 | + for csvFile in csvAppServerFiles: |
| 335 | + extract_ip_data_from_csv(csvFile, app_dict) |
| 336 | + |
| 337 | + ############################################# |
| 338 | + # 3. write owners to json file |
| 339 | + path = os.path.dirname(__file__) |
| 340 | + fileOut = path + '/' + Path(os.path.basename(__file__)).stem + ".json" |
| 341 | + with open(fileOut, "w") as outFH: |
| 342 | + json.dump(transform_owner_dict_to_list(app_dict), outFH, indent=3) |
| 343 | + |
| 344 | + ############################################# |
| 345 | + # 4. Some statistics |
| 346 | + logger.info(f"total #apps: {str(len(app_dict))}") |
| 347 | + appsWithIp = 0 |
| 348 | + for app_id in app_dict: |
| 349 | + appsWithIp += 1 if len(app_dict[app_id].app_servers) > 0 else 0 |
| 350 | + logger.info(f"#apps with ip addresses: {str(appsWithIp)}") |
| 351 | + totalIps = 0 |
| 352 | + for app_id in app_dict: |
| 353 | + totalIps += len(app_dict[app_id].app_servers) |
| 354 | + logger.info(f"#ip addresses in total: {str(totalIps)}") |
| 355 | + |
| 356 | + sys.exit(0) |
0 commit comments