boman-cli 2.1__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {boman-cli-2.1 → boman-cli-2.2.0}/PKG-INFO +7 -1
- {boman-cli-2.1 → boman-cli-2.2.0}/README.md +6 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/boman_cli.egg-info/PKG-INFO +7 -1
- {boman-cli-2.1 → boman-cli-2.2.0}/bomancli/Config.py +17 -2
- {boman-cli-2.1 → boman-cli-2.2.0}/bomancli/auth.py +5 -2
- {boman-cli-2.1 → boman-cli-2.2.0}/bomancli/main.py +80 -4
- {boman-cli-2.1 → boman-cli-2.2.0}/bomancli/utils.py +38 -3
- {boman-cli-2.1 → boman-cli-2.2.0}/bomancli/validation.py +42 -1
- {boman-cli-2.1 → boman-cli-2.2.0}/setup.cfg +1 -1
- {boman-cli-2.1 → boman-cli-2.2.0}/boman_cli.egg-info/SOURCES.txt +0 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/boman_cli.egg-info/dependency_links.txt +0 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/boman_cli.egg-info/entry_points.txt +0 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/boman_cli.egg-info/requires.txt +0 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/boman_cli.egg-info/top_level.txt +0 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/bomancli/_init_.py +0 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/bomancli/base_logger.py +0 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/bomancli/loc_finder.py +0 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/bomancli/templates/template_plan.yaml +0 -0
- {boman-cli-2.1 → boman-cli-2.2.0}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: boman-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: CLI tool of boman.ai
|
|
5
5
|
Home-page: https://boman.ai
|
|
6
6
|
Author: Sumeru Software Solutions Pvt. Ltd.
|
|
@@ -83,6 +83,12 @@ Example: boman-cli -a run -zap_session_script ./session.js
|
|
|
83
83
|
|
|
84
84
|
### Release Note:
|
|
85
85
|
|
|
86
|
+
### V2.2.0
|
|
87
|
+
- New scan added: IaC.
|
|
88
|
+
|
|
89
|
+
### V2.1.1
|
|
90
|
+
- Ignore files or directory for SAST and SCA
|
|
91
|
+
|
|
86
92
|
### V2.1
|
|
87
93
|
- New scan added: SBOM.
|
|
88
94
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: boman-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: CLI tool of boman.ai
|
|
5
5
|
Home-page: https://boman.ai
|
|
6
6
|
Author: Sumeru Software Solutions Pvt. Ltd.
|
|
@@ -83,6 +83,12 @@ Example: boman-cli -a run -zap_session_script ./session.js
|
|
|
83
83
|
|
|
84
84
|
### Release Note:
|
|
85
85
|
|
|
86
|
+
### V2.2.0
|
|
87
|
+
- New scan added: IaC.
|
|
88
|
+
|
|
89
|
+
### V2.1.1
|
|
90
|
+
- Ignore files or directory for SAST and SCA
|
|
91
|
+
|
|
86
92
|
### V2.1
|
|
87
93
|
- New scan added: SBOM.
|
|
88
94
|
|
|
@@ -25,6 +25,7 @@ class Config:
|
|
|
25
25
|
sast_upload_status = None
|
|
26
26
|
sast_message = None
|
|
27
27
|
sast_errors = None
|
|
28
|
+
sast_ignore = False
|
|
28
29
|
|
|
29
30
|
dast_present = None
|
|
30
31
|
dast_target = None
|
|
@@ -56,6 +57,8 @@ class Config:
|
|
|
56
57
|
sca_upload_status = None
|
|
57
58
|
sca_message = None
|
|
58
59
|
sca_errors = None
|
|
60
|
+
sca_ignore = False
|
|
61
|
+
sca_exclude_paths=None
|
|
59
62
|
|
|
60
63
|
app_token = None
|
|
61
64
|
customer_token = None
|
|
@@ -115,7 +118,7 @@ class Config:
|
|
|
115
118
|
|
|
116
119
|
log_level = "INFO"
|
|
117
120
|
|
|
118
|
-
version = 'v2.
|
|
121
|
+
version = 'v2.2.0'
|
|
119
122
|
|
|
120
123
|
boman_config_file = 'boman.yaml'
|
|
121
124
|
|
|
@@ -159,4 +162,16 @@ class Config:
|
|
|
159
162
|
sbom_errors=None
|
|
160
163
|
sbom_upload_status=None
|
|
161
164
|
sbom_scan_status=None
|
|
162
|
-
sbom_target=None
|
|
165
|
+
sbom_target=None
|
|
166
|
+
|
|
167
|
+
#IAC scanning
|
|
168
|
+
iac_scan_present=None
|
|
169
|
+
iac_scan_build_dir=None
|
|
170
|
+
iac_scan_message=None
|
|
171
|
+
iac_scan_response=None
|
|
172
|
+
iac_scan_errors=None
|
|
173
|
+
iac_scan_upload_status=None
|
|
174
|
+
iac_scan_status=None
|
|
175
|
+
iac_scan_type=None
|
|
176
|
+
iac_scan_target=None
|
|
177
|
+
iac_valid_exit_status = [0,60,50,40,30,20]
|
|
@@ -46,8 +46,9 @@ def authorize():
|
|
|
46
46
|
|
|
47
47
|
logging.info(f"Boman opted for: {Config.sca_lang} scan")
|
|
48
48
|
logging.info('Authenticating with boman server')
|
|
49
|
-
data = {'app_token': Config.app_token, 'customer_token': Config.customer_token, 'sast':Config.sast_present,"dast":Config.dast_present,"dast_type":Config.dast_type,"dast_auth_enabled":Config.dast_auth_present,"sast_langs":Config.sast_lang,"sca":Config.sca_present,"sca_langs":Config.sca_lang,"sca_scan_type":Config.sca_type,"secret_scan":Config.secret_scan_present,'container_scan': Config.con_scan_present,'container_scan_type': Config.con_scan_type,"sbom":Config.sbom_present}
|
|
49
|
+
data = {'app_token': Config.app_token, 'customer_token': Config.customer_token, 'sast':Config.sast_present,"dast":Config.dast_present,"dast_type":Config.dast_type,"dast_auth_enabled":Config.dast_auth_present,"sast_langs":Config.sast_lang,"sca":Config.sca_present,"sca_langs":Config.sca_lang,"sca_scan_type":Config.sca_type,"secret_scan":Config.secret_scan_present,'container_scan': Config.con_scan_present,'container_scan_type': Config.con_scan_type,"sbom":Config.sbom_present,'iac':Config.iac_scan_present}
|
|
50
50
|
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
|
|
51
|
+
# logging.info(data)
|
|
51
52
|
try:
|
|
52
53
|
res = requests.post(url, json=data, headers=headers)
|
|
53
54
|
#print('req:', json.dumps(data))
|
|
@@ -60,6 +61,7 @@ def authorize():
|
|
|
60
61
|
try:
|
|
61
62
|
json_response = json.loads(res.content)
|
|
62
63
|
logging.info('Authentication Done')
|
|
64
|
+
# logging.info(json_response)
|
|
63
65
|
except:
|
|
64
66
|
logging.info('Authentication Failure')
|
|
65
67
|
try:
|
|
@@ -70,7 +72,8 @@ def authorize():
|
|
|
70
72
|
Config.scan_token = json_response['data']['scan_token']
|
|
71
73
|
Config.scan_name = json_response['data']['scan_name']
|
|
72
74
|
Config.con_scan_response = json_response['data']['cs']
|
|
73
|
-
Config.sbom_response = json_response['data']['sbom']
|
|
75
|
+
Config.sbom_response = json_response['data']['sbom']
|
|
76
|
+
Config.iac_scan_response = json_response['data']['iac']
|
|
74
77
|
|
|
75
78
|
return 1
|
|
76
79
|
except:
|
|
@@ -125,6 +125,12 @@ def runImage(data=None,type=None):
|
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
if type == 'SAST':
|
|
128
|
+
if Config.sast_ignore:
|
|
129
|
+
if Utils.copy_file('.bomanignore','.semgrepignore'):
|
|
130
|
+
logging.info("Files and Folders to be ignored is added to .semgrepignore file")
|
|
131
|
+
else:
|
|
132
|
+
logging.error(".bomanignore file was not found")
|
|
133
|
+
exit(4)
|
|
128
134
|
target_file = Config.sast_target
|
|
129
135
|
Utils.checkImageAlreadyExsist(docker_image)
|
|
130
136
|
|
|
@@ -220,7 +226,8 @@ def runImage(data=None,type=None):
|
|
|
220
226
|
|
|
221
227
|
|
|
222
228
|
try:
|
|
223
|
-
|
|
229
|
+
if Config.sast_ignore:
|
|
230
|
+
os.remove('.semgrepignore')
|
|
224
231
|
if will_generate_output == 1:
|
|
225
232
|
#logging.info('WILL GENERATE OUTPUT')
|
|
226
233
|
if uploadReport(output_file,tool_name,tool_id,scan_details_id,'SAST'):
|
|
@@ -446,7 +453,12 @@ def runImage(data=None,type=None):
|
|
|
446
453
|
container_output = docker.containers.run(docker_image, command_line, volumes={Config.sca_build_dir: {
|
|
447
454
|
'bind': data['bind']}}, user=uid)
|
|
448
455
|
logging.info('[SUCCESS]: %s Scan Completed',tool_name)
|
|
449
|
-
else:
|
|
456
|
+
else:
|
|
457
|
+
if Config.sca_ignore:
|
|
458
|
+
Config.sca_exclude_files = Utils.read_bomanignore('.bomanignore')
|
|
459
|
+
if Config.sca_exclude_files:
|
|
460
|
+
command_line = f'{command_line} {Config.sca_exclude_files}'
|
|
461
|
+
logging.info('Comment for SCA with ignore %s',command_line)
|
|
450
462
|
Config.build_dir = Config.sca_build_dir
|
|
451
463
|
container_output = docker.containers.run(docker_image, command_line, volumes={Config.sca_build_dir: {
|
|
452
464
|
'bind': data['bind']}}, user=uid)
|
|
@@ -508,7 +520,7 @@ def runImage(data=None,type=None):
|
|
|
508
520
|
except errors.ContainerError as exc:
|
|
509
521
|
logging.error('Some Error recorded while scanning %s',tool_name)
|
|
510
522
|
logging.error('%s',str(exc))
|
|
511
|
-
msg='\n The following error has been recorded while scanning
|
|
523
|
+
msg='\n The following error has been recorded while scanning Container'
|
|
512
524
|
Config.con_scan_status ='Failed'
|
|
513
525
|
Config.con_scan_errors ='Some Error recorded while scanning [',str(exc),']'
|
|
514
526
|
Utils.logError(msg,str(exc))
|
|
@@ -576,6 +588,54 @@ def runImage(data=None,type=None):
|
|
|
576
588
|
Config.sbom_message ='Error recorded while uploading the report of SBOM, Please check your directory for the files.' ## need to change logic here -- MM
|
|
577
589
|
msg = 'Error recorded while uploading the report'
|
|
578
590
|
Utils.logError(msg,str(e))
|
|
591
|
+
|
|
592
|
+
if type == 'iac':
|
|
593
|
+
Utils.checkImageAlreadyExsist(docker_image)
|
|
594
|
+
logging.info('Running %s',tool_name)
|
|
595
|
+
|
|
596
|
+
try:
|
|
597
|
+
command_line = "% s" % command_line.format(target = '/src'+Config.iac_scan_target)
|
|
598
|
+
Config.build_dir = Config.iac_scan_build_dir
|
|
599
|
+
container_output = docker.containers.run(docker_image, command_line, volumes={Config.iac_scan_build_dir: {
|
|
600
|
+
'bind': data['bind']}}, user=uid)
|
|
601
|
+
logging.info('[SUCCESS]: %s Scan Completed',tool_name)
|
|
602
|
+
Config.iac_scan_message ='IaC Scan completed'
|
|
603
|
+
Config.iac_scan_status ='Completed'
|
|
604
|
+
except errors.ContainerError as exc:
|
|
605
|
+
if exc.exit_status in Config.iac_valid_exit_status:
|
|
606
|
+
Config.iac_scan_message ='IaC scan completed'
|
|
607
|
+
Config.iac_scan_status ='Completed'
|
|
608
|
+
logging.info('[SUCCESS]: %s Scan Completed',tool_name)
|
|
609
|
+
logging.info(f'IaC: {exc.stderr}')
|
|
610
|
+
else:
|
|
611
|
+
logging.error('Some Error recorded while scanning %s',tool_name)
|
|
612
|
+
logging.error('%s',str(exc))
|
|
613
|
+
msg='\n The following error has been recorded while scanning IaC'
|
|
614
|
+
Config.iac_scan_status ='Failed'
|
|
615
|
+
Config.iac_scan_errors ='Some Error recorded while scanning [',str(exc),']'
|
|
616
|
+
Utils.logError(msg,str(exc))
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
if will_generate_output == 1:
|
|
620
|
+
logging.info('Uploading %s to the server',output_file)
|
|
621
|
+
if uploadReport(output_file,tool_name,tool_id,scan_details_id,'iac'):
|
|
622
|
+
Config.iac_scan_status ='Completed'
|
|
623
|
+
Config.iac_scan_upload_status = 'Completed'
|
|
624
|
+
Config.iac_scan_message ='Scan Completed'
|
|
625
|
+
else:
|
|
626
|
+
Config.iac_scan_status ='Failed'
|
|
627
|
+
Config.iac_scan_upload_status = 'Failed'
|
|
628
|
+
Config.iac_scan_message ='Error occured while uploading the report, Please check the cli logs'
|
|
629
|
+
else:
|
|
630
|
+
logging.error('Cant upload files to the server',tool_name)
|
|
631
|
+
Config.iac_scan_message ='Cant upload files to the server for IaC Scan,Please check your directory for the files.'
|
|
632
|
+
|
|
633
|
+
except EnvironmentError as e:
|
|
634
|
+
logging.error('Error recorded while uploading the report %s',tool_name)
|
|
635
|
+
logging.error('%s',str(e))
|
|
636
|
+
Config.iac_scan_message ='Error recorded while uploading the report of IaC Scan, Please check your directory for the files.' ## need to change logic here -- MM
|
|
637
|
+
msg = 'Error recorded while uploading the report'
|
|
638
|
+
Utils.logError(msg,str(e))
|
|
579
639
|
|
|
580
640
|
|
|
581
641
|
#### function to upload the test report to the server with other data -- MM ------------------------------------
|
|
@@ -607,6 +667,9 @@ def uploadReport(filename,toolname,tool_id,scan_details_id,type):
|
|
|
607
667
|
elif type =="sbom":
|
|
608
668
|
message = Config.sbom_message
|
|
609
669
|
errors = Config.sbom_errors
|
|
670
|
+
elif type =="iac":
|
|
671
|
+
message = Config.iac_scan_message
|
|
672
|
+
errors = Config.iac_scan_errors
|
|
610
673
|
except:
|
|
611
674
|
message = 'NA'
|
|
612
675
|
errors = 'NA'
|
|
@@ -787,7 +850,7 @@ def main():
|
|
|
787
850
|
|
|
788
851
|
init()
|
|
789
852
|
Validation.yamlValidation()
|
|
790
|
-
if Config.secret_scan_present == True or Config.sast_present is True or Config.dast_present is True or Config.sca_present is True or Config.con_scan_present is True or Config.sbom_present:
|
|
853
|
+
if Config.secret_scan_present == True or Config.sast_present is True or Config.dast_present is True or Config.sca_present is True or Config.con_scan_present is True or Config.sbom_present or Config.iac_scan_present:
|
|
791
854
|
Utils.testServer()
|
|
792
855
|
else:
|
|
793
856
|
content = Auth.authorize()
|
|
@@ -939,6 +1002,19 @@ def main():
|
|
|
939
1002
|
runImage(data=data,type='sbom')
|
|
940
1003
|
else:
|
|
941
1004
|
logging.info('Ignoring SBOM')
|
|
1005
|
+
|
|
1006
|
+
if Config.iac_scan_present is True:
|
|
1007
|
+
logging.info("Preparing IaC Scan requirements")
|
|
1008
|
+
|
|
1009
|
+
for data in Config.iac_scan_response:
|
|
1010
|
+
|
|
1011
|
+
if data['scan_status'] == 0 :
|
|
1012
|
+
logging.info('No IaC Scan Configuration found from SaaS')
|
|
1013
|
+
logging.info('Ignoring IaC Scan')
|
|
1014
|
+
else:
|
|
1015
|
+
runImage(data=data,type='iac')
|
|
1016
|
+
else:
|
|
1017
|
+
logging.info('Ignoring IaC Scan')
|
|
942
1018
|
|
|
943
1019
|
exitFunction()
|
|
944
1020
|
return 1
|
|
@@ -339,8 +339,16 @@ def showSummary():
|
|
|
339
339
|
logging.info('SCAN MESSAGE : %s', Config.sbom_message)
|
|
340
340
|
logging.info('-------------------------------------')
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
342
|
+
logging.info('-------- IaC SCAN STATUS --------- ')
|
|
343
|
+
if Config.iac_scan_present:
|
|
344
|
+
logging.info('SCAN STATUS: %s',Config.iac_scan_status)
|
|
345
|
+
logging.info('UPLOAD STATUS: %s',Config.iac_scan_upload_status)
|
|
346
|
+
logging.info('SCAN MESSAGE : %s', Config.iac_scan_message)
|
|
347
|
+
|
|
348
|
+
logging.info('ERRORS: %s',Config.iac_scan_errors)
|
|
349
|
+
else:
|
|
350
|
+
logging.info('SCAN MESSAGE : %s', Config.iac_scan_message)
|
|
351
|
+
logging.info('-------------------------------------')
|
|
344
352
|
# logging.info(Config.sast_message)
|
|
345
353
|
# logging.info(Config.dast_message)
|
|
346
354
|
# logging.info(Config.sca_message)
|
|
@@ -889,4 +897,31 @@ def uploadLogs():
|
|
|
889
897
|
|
|
890
898
|
def remove_leading_slash(s):
|
|
891
899
|
# Check if the first character is a slash and remove it
|
|
892
|
-
return s[1:] if s.startswith('/') else s
|
|
900
|
+
return s[1:] if s.startswith('/') else s
|
|
901
|
+
|
|
902
|
+
def read_bomanignore(file_path):
|
|
903
|
+
exclude_paths = []
|
|
904
|
+
try:
|
|
905
|
+
with open(file_path, 'r') as file:
|
|
906
|
+
for line in file:
|
|
907
|
+
# Skip comments and empty lines
|
|
908
|
+
line = line.strip()
|
|
909
|
+
if line and not line.startswith('#'):
|
|
910
|
+
full_command = " --exclude "+line
|
|
911
|
+
exclude_paths.append(full_command)
|
|
912
|
+
except FileNotFoundError:
|
|
913
|
+
logging.info(f"Error: {file_path} not found.")
|
|
914
|
+
return False
|
|
915
|
+
# Join the list into a space separated string
|
|
916
|
+
return ''.join(exclude_paths)
|
|
917
|
+
|
|
918
|
+
def copy_file(source_path, dest_path):
|
|
919
|
+
try:
|
|
920
|
+
with open(source_path, 'r') as source_file:
|
|
921
|
+
source_content = source_file.read()
|
|
922
|
+
with open(dest_path, 'w') as dest_file:
|
|
923
|
+
dest_file.write(source_content)
|
|
924
|
+
return True # Successfully copied
|
|
925
|
+
except FileNotFoundError:
|
|
926
|
+
logging.info(f"Error: {source_path} not found.")
|
|
927
|
+
return False # Source file not found
|
|
@@ -61,6 +61,13 @@ def yamlValidation():
|
|
|
61
61
|
except KeyError:
|
|
62
62
|
logging.info('work dir not specified in config, choosing the default sast working directory')
|
|
63
63
|
Config.sast_build_dir = os.getcwd()+'/'
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
logging.info('Ignoring file and folder check')
|
|
67
|
+
Config.sast_ignore = Config.config_data['SAST']['ignore_files']
|
|
68
|
+
except KeyError:
|
|
69
|
+
logging.info('ignore_files config was not found')
|
|
70
|
+
|
|
64
71
|
|
|
65
72
|
#logging.info('snyk is choosen, and the env var declared was %s', str('s'))
|
|
66
73
|
|
|
@@ -170,6 +177,11 @@ def yamlValidation():
|
|
|
170
177
|
# ##after sbom and lockfile type check
|
|
171
178
|
# Config.sca_build_dir = os.getcwd()+'/'
|
|
172
179
|
|
|
180
|
+
try:
|
|
181
|
+
logging.info('Ignoring file and folder check')
|
|
182
|
+
Config.sca_ignore = Config.config_data['SCA']['ignore_files']
|
|
183
|
+
except KeyError:
|
|
184
|
+
logging.info('ignore_files config was not found')
|
|
173
185
|
|
|
174
186
|
try:
|
|
175
187
|
logging.info('Configuring target for SCA')
|
|
@@ -282,7 +294,36 @@ def yamlValidation():
|
|
|
282
294
|
Config.sbom_present = False
|
|
283
295
|
logging.warning('SBOM is not properly configured. Cant read the "sbom" configuration.')
|
|
284
296
|
Config.sbom_message ='sbom is not properly configured'
|
|
285
|
-
|
|
297
|
+
|
|
298
|
+
# Validation of boman.yaml for IaC scanning
|
|
299
|
+
try:
|
|
300
|
+
if "IAC" in Config.config_data:
|
|
301
|
+
Config.iac_scan_present = True
|
|
302
|
+
Config.iac_scan_build_dir= os.getcwd()+'/'
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
logging.info('Configuring target for IaC Scan.')
|
|
306
|
+
Config.iac_scan_target = Config.config_data['IAC']['target']
|
|
307
|
+
logging.info(f'Target configured for IaC Scan: {Config.iac_scan_target}')
|
|
308
|
+
except KeyError:
|
|
309
|
+
Config.iac_scan_target="/"
|
|
310
|
+
logging.info("KEY ERROR")
|
|
311
|
+
logging.info('target is missing for IaC Scan. target configured to default path')
|
|
312
|
+
except TypeError:
|
|
313
|
+
Config.iac_scan_target="/"
|
|
314
|
+
logging.info("TYPE ERROR")
|
|
315
|
+
logging.info('target is missing for IaC Scan. target configured to default path')
|
|
316
|
+
|
|
317
|
+
logging.info('IaC Scan is properly configured and ready to scan')
|
|
318
|
+
Config.iac_scan_message ='IaC Scan is properly configured and ready to scan'
|
|
319
|
+
else:
|
|
320
|
+
Config.iac_scan_present = False
|
|
321
|
+
logging.warning('IaC Scan is not properly configured. Cant read the "IAC" configuration.')
|
|
322
|
+
Config.iac_scan_message ='IaC Scan is not properly configured'
|
|
323
|
+
except:
|
|
324
|
+
Config.iac_scan_present = False
|
|
325
|
+
logging.warning('IaC Scan is not properly configured. Cant read the "IAC" configuration.')
|
|
326
|
+
Config.iac_scan_message ='IaC Scan is not properly configured'
|
|
286
327
|
|
|
287
328
|
|
|
288
329
|
## need to use lingudetect here, but the results are not trustable and misleading ------ MM -------------------
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|