Skip to content

Commit 7ba57d3

Browse files
authored
Merge pull request CactuseSecurity#3792 from tpurschke/feat/issue_3761_import-app-data
Feat/issue 3761 import app data
2 parents 9713c6c + f6747cc commit 7ba57d3

File tree

3 files changed

+358
-1
lines changed

3 files changed

+358
-1
lines changed

roles/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
## files generated by popular Visual Studio add-ons.
33
##
44
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5-
5+
**/.venv/
66
# User-specific files
77
*.rsuser
88
*.suo
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
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)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
GitPython>=3.1.0

0 commit comments

Comments
 (0)