boman-cli 2.5.2__tar.gz → 2.5.4__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.5.2 → boman_cli-2.5.4}/PKG-INFO +10 -2
- {boman_cli-2.5.2 → boman_cli-2.5.4}/README.md +9 -1
- {boman_cli-2.5.2 → boman_cli-2.5.4}/boman_cli.egg-info/PKG-INFO +10 -2
- {boman_cli-2.5.2 → boman_cli-2.5.4}/boman_cli.egg-info/SOURCES.txt +1 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/Config.py +7 -2
- boman_cli-2.5.4/bomancli/dev_attribution.py +302 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/loc_finder.py +32 -1
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/main.py +37 -13
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/sbom_enricher.py +7 -5
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/utils.py +73 -5
- {boman_cli-2.5.2 → boman_cli-2.5.4}/setup.cfg +1 -1
- {boman_cli-2.5.2 → boman_cli-2.5.4}/boman_cli.egg-info/dependency_links.txt +0 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/boman_cli.egg-info/entry_points.txt +0 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/boman_cli.egg-info/requires.txt +0 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/boman_cli.egg-info/top_level.txt +0 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/_init_.py +0 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/auth.py +0 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/base_logger.py +0 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/templates/template_plan.yaml +0 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/bomancli/validation.py +0 -0
- {boman_cli-2.5.2 → boman_cli-2.5.4}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boman-cli
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.4
|
|
4
4
|
Summary: CLI tool of boman.ai
|
|
5
5
|
Home-page: https://boman.ai
|
|
6
6
|
Author: Sumeru Software Solutions Pvt. Ltd.
|
|
@@ -105,11 +105,19 @@ Example: boman-cli -a run -zap_session_script ./session.js
|
|
|
105
105
|
2 : Auth error
|
|
106
106
|
3 : Docker/System error
|
|
107
107
|
4 : Misconfig error
|
|
108
|
-
|
|
108
|
+
5 : Failing based on SLA
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
### Release Note:
|
|
113
|
+
|
|
114
|
+
### 2.5.4
|
|
115
|
+
- **CLOC** - New feature. CLI will now generate lines of code of the repo and push it to SaaS.
|
|
116
|
+
- **Dev Attribution** - New feature, Dev names for fetched for the findings and shared with SaaS.
|
|
117
|
+
|
|
118
|
+
### 2.5.3
|
|
119
|
+
- **SLA** - new feature. Build fail SLA has been introduced it can be configured in SaaS platform. Build fails if the condition is met menioned in SaaS. Check Build SLA in SLA menu for more info.
|
|
120
|
+
|
|
113
121
|
### 2.5.1
|
|
114
122
|
- **SBOM** Added New fields as per certin
|
|
115
123
|
|
|
@@ -74,11 +74,19 @@ Example: boman-cli -a run -zap_session_script ./session.js
|
|
|
74
74
|
2 : Auth error
|
|
75
75
|
3 : Docker/System error
|
|
76
76
|
4 : Misconfig error
|
|
77
|
-
|
|
77
|
+
5 : Failing based on SLA
|
|
78
78
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
### Release Note:
|
|
82
|
+
|
|
83
|
+
### 2.5.4
|
|
84
|
+
- **CLOC** - New feature. CLI will now generate lines of code of the repo and push it to SaaS.
|
|
85
|
+
- **Dev Attribution** - New feature, Dev names for fetched for the findings and shared with SaaS.
|
|
86
|
+
|
|
87
|
+
### 2.5.3
|
|
88
|
+
- **SLA** - new feature. Build fail SLA has been introduced it can be configured in SaaS platform. Build fails if the condition is met menioned in SaaS. Check Build SLA in SLA menu for more info.
|
|
89
|
+
|
|
82
90
|
### 2.5.1
|
|
83
91
|
- **SBOM** Added New fields as per certin
|
|
84
92
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: boman-cli
|
|
3
|
-
Version: 2.5.
|
|
3
|
+
Version: 2.5.4
|
|
4
4
|
Summary: CLI tool of boman.ai
|
|
5
5
|
Home-page: https://boman.ai
|
|
6
6
|
Author: Sumeru Software Solutions Pvt. Ltd.
|
|
@@ -105,11 +105,19 @@ Example: boman-cli -a run -zap_session_script ./session.js
|
|
|
105
105
|
2 : Auth error
|
|
106
106
|
3 : Docker/System error
|
|
107
107
|
4 : Misconfig error
|
|
108
|
-
|
|
108
|
+
5 : Failing based on SLA
|
|
109
109
|
|
|
110
110
|
|
|
111
111
|
|
|
112
112
|
### Release Note:
|
|
113
|
+
|
|
114
|
+
### 2.5.4
|
|
115
|
+
- **CLOC** - New feature. CLI will now generate lines of code of the repo and push it to SaaS.
|
|
116
|
+
- **Dev Attribution** - New feature, Dev names for fetched for the findings and shared with SaaS.
|
|
117
|
+
|
|
118
|
+
### 2.5.3
|
|
119
|
+
- **SLA** - new feature. Build fail SLA has been introduced it can be configured in SaaS platform. Build fails if the condition is met menioned in SaaS. Check Build SLA in SLA menu for more info.
|
|
120
|
+
|
|
113
121
|
### 2.5.1
|
|
114
122
|
- **SBOM** Added New fields as per certin
|
|
115
123
|
|
|
@@ -121,7 +121,7 @@ class Config:
|
|
|
121
121
|
|
|
122
122
|
log_level = "INFO"
|
|
123
123
|
|
|
124
|
-
version = 'v2.5.
|
|
124
|
+
version = 'v2.5.4'
|
|
125
125
|
|
|
126
126
|
boman_config_file = 'boman.yaml'
|
|
127
127
|
|
|
@@ -218,7 +218,12 @@ class Config:
|
|
|
218
218
|
reachability_language=None
|
|
219
219
|
|
|
220
220
|
fail_build = False
|
|
221
|
+
sla_fail_build = False
|
|
221
222
|
polling_time = 60
|
|
222
223
|
polling_frequency = 10
|
|
223
224
|
|
|
224
|
-
ml_success = False
|
|
225
|
+
ml_success = False
|
|
226
|
+
|
|
227
|
+
reason_sla_build_fail = []
|
|
228
|
+
|
|
229
|
+
cloc_total_data = None
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import json
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import datetime
|
|
6
|
+
import re
|
|
7
|
+
from docker import errors
|
|
8
|
+
from bomancli.base_logger import logging
|
|
9
|
+
from bomancli.Config import Config
|
|
10
|
+
# --- Git Helper Functions ---
|
|
11
|
+
|
|
12
|
+
def ensure_full_git_history():
|
|
13
|
+
try:
|
|
14
|
+
is_shallow = subprocess.check_output(
|
|
15
|
+
["git", "rev-parse", "--is-shallow-repository"],
|
|
16
|
+
text=True
|
|
17
|
+
).strip()
|
|
18
|
+
|
|
19
|
+
if is_shallow == "true":
|
|
20
|
+
logging.info("[INFO] Shallow repository detected. Fetching full history...")
|
|
21
|
+
subprocess.run(
|
|
22
|
+
["git", "fetch", "--prune", "--unshallow"],
|
|
23
|
+
check=False
|
|
24
|
+
)
|
|
25
|
+
else:
|
|
26
|
+
logging.info("[INFO] Repository already has full history.")
|
|
27
|
+
|
|
28
|
+
except Exception as e:
|
|
29
|
+
logging.error(f"[WARN] Could not determine git history depth: {e}", file=sys.stderr)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_git_command(cmd, cwd=None):
|
|
36
|
+
"""Run a git command and return output."""
|
|
37
|
+
try:
|
|
38
|
+
if cwd and not os.path.exists(cwd):
|
|
39
|
+
return None, "cwd_not_found"
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
cmd,
|
|
42
|
+
stdout=subprocess.PIPE,
|
|
43
|
+
stderr=subprocess.PIPE,
|
|
44
|
+
text=True,
|
|
45
|
+
check=False,
|
|
46
|
+
cwd=cwd,
|
|
47
|
+
encoding='utf-8',
|
|
48
|
+
errors='replace'
|
|
49
|
+
)
|
|
50
|
+
if result.returncode != 0:
|
|
51
|
+
return None, result.stderr.strip()
|
|
52
|
+
return result.stdout.strip(), None
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logging.error(f"Error running git command {' '.join(cmd)}: {e}")
|
|
55
|
+
return None, str(e)
|
|
56
|
+
|
|
57
|
+
def get_branch_info(repo_root="."):
|
|
58
|
+
"""Fetch current branch name, commit, and remote."""
|
|
59
|
+
branch, _ = run_git_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_root)
|
|
60
|
+
commit, _ = run_git_command(["git", "rev-parse", "HEAD"], cwd=repo_root)
|
|
61
|
+
remote, _ = run_git_command(["git", "config", "--get", "remote.origin.url"], cwd=repo_root)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
"name": branch,
|
|
65
|
+
"head_commit": commit,
|
|
66
|
+
"remote_url": remote
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# --- Blame Logic ---
|
|
70
|
+
|
|
71
|
+
def get_owner_from_lines(file_path, start_line, end_line):
|
|
72
|
+
# Ensure file exists
|
|
73
|
+
if not os.path.isfile(file_path):
|
|
74
|
+
return None, "file_not_found_on_disk"
|
|
75
|
+
|
|
76
|
+
cmd = [
|
|
77
|
+
"git", "blame",
|
|
78
|
+
"-w", "-M", "-C",
|
|
79
|
+
"-L", f"{start_line},{end_line}",
|
|
80
|
+
"--line-porcelain",
|
|
81
|
+
"--", file_path
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
result, error = run_git_command(cmd)
|
|
85
|
+
|
|
86
|
+
if error:
|
|
87
|
+
return None, error
|
|
88
|
+
|
|
89
|
+
line_authors = []
|
|
90
|
+
|
|
91
|
+
line_authors = []
|
|
92
|
+
current = {}
|
|
93
|
+
|
|
94
|
+
for line in result.splitlines():
|
|
95
|
+
# Check for new block start (Hash Line)
|
|
96
|
+
# Format: <40-hex-sha> <orig_line> <final_line> [<group_lines>]
|
|
97
|
+
if re.match(r'^[a-f0-9]{40}\s', line):
|
|
98
|
+
# If we have a previous block with sufficient data, save it
|
|
99
|
+
if "author" in current:
|
|
100
|
+
line_authors.append(current)
|
|
101
|
+
# Start new block
|
|
102
|
+
current = {}
|
|
103
|
+
parts = line.split(" ")
|
|
104
|
+
current["commit"] = parts[0]
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if line.startswith("author "):
|
|
108
|
+
current["author"] = line.replace("author ", "")
|
|
109
|
+
elif line.startswith("author-mail "):
|
|
110
|
+
current["email"] = line.replace("author-mail ", "").strip("<>")
|
|
111
|
+
elif line.startswith("author-time "):
|
|
112
|
+
current["author_time"] = int(line.replace("author-time ", ""))
|
|
113
|
+
elif line.startswith("committer-time "):
|
|
114
|
+
pass
|
|
115
|
+
elif line.startswith("summary "):
|
|
116
|
+
current["summary"] = line.replace("summary ", "")
|
|
117
|
+
elif line.startswith("\t"): # actual code line
|
|
118
|
+
current["code"] = line.strip()
|
|
119
|
+
|
|
120
|
+
# Capture the last block
|
|
121
|
+
if "author" in current:
|
|
122
|
+
line_authors.append(current)
|
|
123
|
+
|
|
124
|
+
if not line_authors:
|
|
125
|
+
return None, f"no_authors_parsed_from_blame. Raw output start: {result[:200]!r}"
|
|
126
|
+
|
|
127
|
+
# Aggregate stats for this specific block
|
|
128
|
+
unique_authors = {}
|
|
129
|
+
|
|
130
|
+
for entry in line_authors:
|
|
131
|
+
email = entry.get("email")
|
|
132
|
+
if email not in unique_authors:
|
|
133
|
+
unique_authors[email] = {
|
|
134
|
+
"name": entry.get("author"),
|
|
135
|
+
"email": email,
|
|
136
|
+
"commit": entry.get("commit"),
|
|
137
|
+
"line_count": 0,
|
|
138
|
+
"timestamps": []
|
|
139
|
+
}
|
|
140
|
+
unique_authors[email]["line_count"] += 1
|
|
141
|
+
unique_authors[email]["timestamps"].append(entry.get("author_time"))
|
|
142
|
+
|
|
143
|
+
# Sort by line count
|
|
144
|
+
sorted_authors = sorted(unique_authors.values(), key=lambda x: x["line_count"], reverse=True)
|
|
145
|
+
|
|
146
|
+
if not sorted_authors:
|
|
147
|
+
return None, "no_sorted_authors"
|
|
148
|
+
|
|
149
|
+
# Determine primary owner for this finding
|
|
150
|
+
primary = sorted_authors[0]
|
|
151
|
+
|
|
152
|
+
# Calculate most recent time
|
|
153
|
+
all_timestamps = [t for a in unique_authors.values() for t in a["timestamps"]]
|
|
154
|
+
most_recent_ts = max(all_timestamps) if all_timestamps else 0
|
|
155
|
+
most_recent_str = datetime.datetime.fromtimestamp(most_recent_ts).strftime("%Y-%m-%d %H:%M:%S")
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"file": file_path,
|
|
159
|
+
"lines": f"{start_line}-{end_line}",
|
|
160
|
+
"authors": sorted_authors,
|
|
161
|
+
"primary_owner": primary["name"],
|
|
162
|
+
"primary_email": primary["email"],
|
|
163
|
+
"primary_commit": primary["commit"],
|
|
164
|
+
"updated_at": most_recent_str
|
|
165
|
+
}, None
|
|
166
|
+
|
|
167
|
+
# --- Main Enrichment Logic ---
|
|
168
|
+
|
|
169
|
+
def normalize_path(path):
|
|
170
|
+
# Remove leading prefixes often found in container scans
|
|
171
|
+
if path.startswith("/src/"):
|
|
172
|
+
return path[5:]
|
|
173
|
+
elif path.startswith("src/"):
|
|
174
|
+
return path[4:]
|
|
175
|
+
return path.lstrip("/")
|
|
176
|
+
|
|
177
|
+
def enrich_files(input_path, output_path):
|
|
178
|
+
# 1. Load Input
|
|
179
|
+
|
|
180
|
+
logging.info(f"Loading {input_path}...")
|
|
181
|
+
|
|
182
|
+
#logging.info('checking git swallow')
|
|
183
|
+
|
|
184
|
+
ensure_full_git_history()
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
with open(input_path, "r", encoding="utf-8") as f:
|
|
188
|
+
data = json.load(f)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logging.error(f"Error reading input file: {e}")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# Handle if root is list
|
|
194
|
+
results = data.get("results", []) if isinstance(data, dict) else data
|
|
195
|
+
if isinstance(data, list):
|
|
196
|
+
# normalize structure
|
|
197
|
+
data = {"results": results}
|
|
198
|
+
|
|
199
|
+
# 2. Prepare Globals
|
|
200
|
+
git_users_registry = {} # key: email, value: stats
|
|
201
|
+
|
|
202
|
+
logging.info(f"Processing {len(results)} findings...")
|
|
203
|
+
|
|
204
|
+
# 3. Process Findings
|
|
205
|
+
for idx, result in enumerate(results):
|
|
206
|
+
path = normalize_path(result.get("path", ""))
|
|
207
|
+
|
|
208
|
+
# Determine lines
|
|
209
|
+
start_line = None
|
|
210
|
+
if isinstance(result.get("start"), dict):
|
|
211
|
+
start_line = result.get("start", {}).get("line")
|
|
212
|
+
else:
|
|
213
|
+
start_line = result.get("start_line") or result.get("start") # fallback
|
|
214
|
+
|
|
215
|
+
end_line = None
|
|
216
|
+
if isinstance(result.get("end"), dict):
|
|
217
|
+
end_line = result.get("end", {}).get("line")
|
|
218
|
+
else:
|
|
219
|
+
end_line = result.get("end_line") or result.get("end") or start_line
|
|
220
|
+
|
|
221
|
+
if not path or not start_line:
|
|
222
|
+
result["attribution"] = {"status": "skipped_invalid_path_or_line"}
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
if not os.path.exists(path):
|
|
226
|
+
result["attribution"] = {"status": "file_not_found", "original_path": result.get("path")}
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
# Get Blame
|
|
230
|
+
blame_info, blame_error = get_owner_from_lines(path, int(start_line), int(end_line))
|
|
231
|
+
|
|
232
|
+
if blame_info:
|
|
233
|
+
# Enriched Attribution Field
|
|
234
|
+
result["attribution"] = {
|
|
235
|
+
"developer": blame_info["primary_owner"],
|
|
236
|
+
"email": blame_info["primary_email"],
|
|
237
|
+
"commit": blame_info["primary_commit"],
|
|
238
|
+
"updated_at": blame_info["updated_at"],
|
|
239
|
+
"confidence": "high",
|
|
240
|
+
"status": "attributed",
|
|
241
|
+
"all_authors": blame_info["authors"] # optional detail
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Update Global Registry
|
|
245
|
+
for author in blame_info["authors"]:
|
|
246
|
+
email = author["email"]
|
|
247
|
+
if email not in git_users_registry:
|
|
248
|
+
git_users_registry[email] = {
|
|
249
|
+
"name": author["name"],
|
|
250
|
+
"email": email,
|
|
251
|
+
"total_attributed_findings": 0,
|
|
252
|
+
"last_active_ts": 0
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
# Update stats
|
|
256
|
+
reg = git_users_registry[email]
|
|
257
|
+
if author["name"] == blame_info["primary_owner"]:
|
|
258
|
+
# Only count as 'attributed finding' if they are the primary owner?
|
|
259
|
+
# Or count participation? Let's count if they are primary owner.
|
|
260
|
+
reg["total_attributed_findings"] += 1
|
|
261
|
+
|
|
262
|
+
# Update activity time
|
|
263
|
+
# author["timestamps"] is a list of ts
|
|
264
|
+
if author["timestamps"]:
|
|
265
|
+
max_ts = max(author["timestamps"])
|
|
266
|
+
if max_ts > reg["last_active_ts"]:
|
|
267
|
+
reg["last_active_ts"] = max_ts
|
|
268
|
+
|
|
269
|
+
else:
|
|
270
|
+
result["attribution"] = {"status": "blame_failed", "error": blame_error}
|
|
271
|
+
|
|
272
|
+
# 4. Finalize Globals
|
|
273
|
+
|
|
274
|
+
# Valid Git Users
|
|
275
|
+
final_users = []
|
|
276
|
+
for email, stats in git_users_registry.items():
|
|
277
|
+
stats["last_active"] = datetime.datetime.fromtimestamp(stats["last_active_ts"]).strftime("%Y-%m-%d %H:%M:%S")
|
|
278
|
+
del stats["last_active_ts"]
|
|
279
|
+
final_users.append(stats)
|
|
280
|
+
|
|
281
|
+
data["git_users"] = final_users
|
|
282
|
+
|
|
283
|
+
# Branch Info
|
|
284
|
+
logging.info("Fetching branch info...")
|
|
285
|
+
data["branch_info"] = get_branch_info()
|
|
286
|
+
|
|
287
|
+
# 5. Write Output
|
|
288
|
+
logging.info(f"Writing results to {output_path}...")
|
|
289
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
290
|
+
json.dump(data, f, indent=2)
|
|
291
|
+
logging.info("Done.")
|
|
292
|
+
|
|
293
|
+
def main():
|
|
294
|
+
parser = argparse.ArgumentParser(description="Enrich OpenGrep findings with Git Blame information.")
|
|
295
|
+
parser.add_argument("--input", "-i", required=True, help="Input OpenGrep JSON file")
|
|
296
|
+
parser.add_argument("--output", "-o", default="opengrep_enriched.json", help="Output JSON file")
|
|
297
|
+
|
|
298
|
+
args = parser.parse_args()
|
|
299
|
+
enrich_files(args.input, args.output)
|
|
300
|
+
|
|
301
|
+
if __name__ == "__main__":
|
|
302
|
+
main()
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from docker import errors
|
|
3
|
+
from bomancli.base_logger import logging
|
|
4
|
+
from bomancli.Config import Config
|
|
5
|
+
docker = Config.docker_client
|
|
2
6
|
|
|
3
7
|
|
|
4
8
|
###line counts based on given file extension -- MM
|
|
@@ -59,4 +63,31 @@ def countlines(start,lines=0, header=False, begin_start=None,extentsion='.py'):
|
|
|
59
63
|
|
|
60
64
|
|
|
61
65
|
|
|
62
|
-
#print(countlines('/home/boxuser/box/Vuln-code/',extentsion='.rb'))
|
|
66
|
+
#print(countlines('/home/boxuser/box/Vuln-code/',extentsion='.rb'))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def run_cloc_docker():
|
|
72
|
+
|
|
73
|
+
docker_image="bomanai/cloc:latest"
|
|
74
|
+
userid= Config.userid
|
|
75
|
+
detach=False
|
|
76
|
+
command_line = f"{Config.build_dir} --json --out=/{Config.build_dir}/cloc.json"
|
|
77
|
+
tool_name ='cloc'
|
|
78
|
+
logging.info('Generating loc')
|
|
79
|
+
try:
|
|
80
|
+
|
|
81
|
+
logging.info('Cloc Docker: Preparing %s scan for jenkins env with %s user ', tool_name , userid)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
container_output = docker.containers.run(docker_image, command_line, volumes={Config.sast_build_dir: {
|
|
85
|
+
'bind': Config.sast_build_dir}}, user=userid,detach=detach, remove=True)
|
|
86
|
+
logging.info('Cloc Docker: SUCCESS !!!. Message: %s Scan Completed',tool_name)
|
|
87
|
+
|
|
88
|
+
except errors.ContainerError as exc:
|
|
89
|
+
msg='\n The following error has been recorded while scanning SAST'
|
|
90
|
+
Utils.logError(msg,str(exc))
|
|
91
|
+
logging.error(f'SAST Docker: Failed !!!, Message: Some Error recorded while scanning {str(exc)}')
|
|
92
|
+
Config.sast_scan_status = 'Failed'
|
|
93
|
+
Config.sast_errors = f'Exit Status {exc.exit_status}:{str(exc)}]'
|
|
@@ -31,7 +31,8 @@ from bomancli.sbom_enricher import SBOMEnricher
|
|
|
31
31
|
from bomancli import validation as Validation
|
|
32
32
|
from bomancli import utils as Utils
|
|
33
33
|
from bomancli import auth as Auth
|
|
34
|
-
|
|
34
|
+
from bomancli.dev_attribution import enrich_files
|
|
35
|
+
from bomancli import loc_finder as Loc_finder
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
parser = argparse.ArgumentParser(
|
|
@@ -205,11 +206,23 @@ def runImage(data=None,type=None):
|
|
|
205
206
|
|
|
206
207
|
|
|
207
208
|
|
|
209
|
+
## check whether the tool is opengrep, if yes run gitblame enricher script
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if tool_name == 'Opengrep':
|
|
213
|
+
logging.info("Running Dev attribution on this script")
|
|
214
|
+
enrich_files(output_file,output_file)
|
|
215
|
+
else:
|
|
216
|
+
logging.info("no dev attribution has been run for this")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
208
220
|
try:
|
|
209
221
|
if Config.sast_ignore:
|
|
210
222
|
os.remove('.semgrepignore')
|
|
211
223
|
if will_generate_output == 1:
|
|
212
|
-
|
|
224
|
+
logging.info('WILL GENERATE OUTPUT')
|
|
225
|
+
#output_file ="blame_enriched_report.json"
|
|
213
226
|
if uploadReport(output_file,tool_name,tool_id,scan_details_id,'SAST'):
|
|
214
227
|
Config.sast_scan_status = 'Completed'
|
|
215
228
|
Config.sast_upload_status ='Completed'
|
|
@@ -778,7 +791,8 @@ def uploadReport(filename,toolname,tool_id,scan_details_id,type):
|
|
|
778
791
|
try:
|
|
779
792
|
logging.info('Removing the result file')
|
|
780
793
|
if type != "sbom" and type != "reachability":
|
|
781
|
-
|
|
794
|
+
logging.info("remvoing file here")
|
|
795
|
+
#os.remove(path)
|
|
782
796
|
elif Config.reachability_present is not True and type == 'sbom':
|
|
783
797
|
os.remove(path)
|
|
784
798
|
elif type == "reachability":
|
|
@@ -808,7 +822,7 @@ def uploadReport(filename,toolname,tool_id,scan_details_id,type):
|
|
|
808
822
|
else:
|
|
809
823
|
logging.info('[COMPLETED]: %s Report uploaded Successfully! Report Name: %s',toolname,filename)
|
|
810
824
|
logging.info('Removing the result file')
|
|
811
|
-
|
|
825
|
+
os.remove(path)
|
|
812
826
|
return 1
|
|
813
827
|
elif r.status_code == 401 :
|
|
814
828
|
logging.error('Unauthorized Access while uploading the results. Please check the app/customer tokens')
|
|
@@ -914,10 +928,10 @@ def exitFunction():
|
|
|
914
928
|
x = requests.post(url,json=values)
|
|
915
929
|
response = x.json()
|
|
916
930
|
|
|
917
|
-
if not Config.fail_build:
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
931
|
+
# if not Config.fail_build:
|
|
932
|
+
# logging.info("Fail Build was not configured")
|
|
933
|
+
# Utils.get_summary_of_vulns(response)
|
|
934
|
+
# break
|
|
921
935
|
|
|
922
936
|
if "ml_status" in response.keys():
|
|
923
937
|
if response["ml_status"] == True:
|
|
@@ -1363,43 +1377,53 @@ def default():
|
|
|
1363
1377
|
if main():
|
|
1364
1378
|
logging.info('################################ BOMAN Scanning Done ################################')
|
|
1365
1379
|
logging.info('#####################################################################################')
|
|
1380
|
+
Loc_finder.run_cloc_docker()
|
|
1366
1381
|
Utils.showSummary()
|
|
1382
|
+
#Loc_finder.run_cloc_docker()
|
|
1367
1383
|
Utils.uploadLogs()
|
|
1384
|
+
|
|
1385
|
+
# Sla build failing
|
|
1386
|
+
if Config.sla_fail_build == True:
|
|
1387
|
+
logging.warning("Failing the build for the below reason(s).")
|
|
1388
|
+
for reason in Config.reason_sla_build_fail:
|
|
1389
|
+
logging.warning(f"- {reason}")
|
|
1390
|
+
logging.warning("Please check SLA menu in Boman platform for more details.")
|
|
1391
|
+
exit(5)
|
|
1368
1392
|
|
|
1369
1393
|
## checking the failbuild argument
|
|
1370
1394
|
if args.failBuild == 'fail':
|
|
1371
1395
|
total_vuln = Config.high_count + Config.medium_count + Config.low_count + Config.critical_count
|
|
1372
1396
|
if total_vuln > 0:
|
|
1373
1397
|
logging.warning(f"Failing the build as {args.failBuild} is configured in failBuild argument. Boman has found {total_vuln} vulnerabilities")
|
|
1374
|
-
exit(
|
|
1398
|
+
exit(5)
|
|
1375
1399
|
else:
|
|
1376
1400
|
exit(0)
|
|
1377
1401
|
|
|
1378
1402
|
elif args.failBuild == 'high':
|
|
1379
1403
|
if Config.high_count > 0 or Config.critical_count > 0:
|
|
1380
1404
|
logging.warning(f"Failing the build as {args.failBuild} is configured in failBuild argument. Boman has found {Config.high_count} {args.failBuild} vulnerabilities")
|
|
1381
|
-
exit(
|
|
1405
|
+
exit(5)
|
|
1382
1406
|
else:
|
|
1383
1407
|
exit(0)
|
|
1384
1408
|
|
|
1385
1409
|
elif args.failBuild == 'medium':
|
|
1386
1410
|
if Config.medium_count > 0 or Config.high_count > 0:
|
|
1387
1411
|
logging.warning(f"Failing the build as {args.failBuild} is configured in failBuild argument. Boman has found {Config.medium_count} {args.failBuild} vulnerabilities")
|
|
1388
|
-
exit(
|
|
1412
|
+
exit(5)
|
|
1389
1413
|
else:
|
|
1390
1414
|
exit(0)
|
|
1391
1415
|
|
|
1392
1416
|
elif args.failBuild == 'low':
|
|
1393
1417
|
if Config.low_count > 0 or Config.medium_count > 0 or Config.high_count > 0:
|
|
1394
1418
|
logging.warning(f"Failing the build as {args.failBuild} is configured in failBuild argument. Boman has found {Config.low_count} {args.failBuild} vulnerabilities")
|
|
1395
|
-
exit(
|
|
1419
|
+
exit(5)
|
|
1396
1420
|
else:
|
|
1397
1421
|
exit(0)
|
|
1398
1422
|
|
|
1399
1423
|
elif args.failBuild == 'critical':
|
|
1400
1424
|
if Config.critical_count > 0:
|
|
1401
1425
|
logging.warning(f"Failing the build as {args.failBuild} is configured in failBuild argument. Boman has found {Config.critical_count} {args.failBuild} vulnerabilities")
|
|
1402
|
-
exit(
|
|
1426
|
+
exit(5)
|
|
1403
1427
|
else:
|
|
1404
1428
|
exit(0)
|
|
1405
1429
|
|
|
@@ -6,6 +6,8 @@ import asyncio
|
|
|
6
6
|
import aiohttp
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from datetime import datetime
|
|
9
|
+
from bomancli.base_logger import logging
|
|
10
|
+
from bomancli.Config import Config
|
|
9
11
|
# -----------------------------------------------------
|
|
10
12
|
# BASE FETCHERS (Modular Structure)
|
|
11
13
|
# -----------------------------------------------------
|
|
@@ -99,7 +101,7 @@ class MavenFetcher(BaseFetcher):
|
|
|
99
101
|
# --- FIX: Ensure the API response is a dictionary ---
|
|
100
102
|
if not isinstance(j, dict):
|
|
101
103
|
# If j is a string (non-JSON response), we skip processing and return empty data
|
|
102
|
-
|
|
104
|
+
logging.error(f"[WARN] Maven API for {name} returned non-JSON data or failed parsing.")
|
|
103
105
|
return data
|
|
104
106
|
# ----------------------------------------------------
|
|
105
107
|
|
|
@@ -334,7 +336,7 @@ class SBOMEnricher:
|
|
|
334
336
|
|
|
335
337
|
# Store the map for quick lookup later
|
|
336
338
|
self._dependency_map = dependency_map
|
|
337
|
-
|
|
339
|
+
logging.info(f"Preprocessed dependency map containing {len(dependency_map)} components.")
|
|
338
340
|
|
|
339
341
|
|
|
340
342
|
|
|
@@ -354,7 +356,7 @@ class SBOMEnricher:
|
|
|
354
356
|
fetcher_instance = Fetcher(session)
|
|
355
357
|
return await fetcher_instance.fetch_metadata(package_name, version)
|
|
356
358
|
except Exception as e:
|
|
357
|
-
|
|
359
|
+
logging.error(f"[ERROR] Fetcher failed for {package_type} ({package_name}): {e}")
|
|
358
360
|
return {"description": "", "release_date": ""}
|
|
359
361
|
else:
|
|
360
362
|
# Fallback for unknown types (e.g., github, generic files)
|
|
@@ -480,7 +482,7 @@ class SBOMEnricher:
|
|
|
480
482
|
|
|
481
483
|
# Check for components before running async
|
|
482
484
|
if not self.get_components():
|
|
483
|
-
|
|
485
|
+
logging.info("No components found in SBOM to enrich.")
|
|
484
486
|
return self.sbom_data
|
|
485
487
|
|
|
486
488
|
# --- NEW STEP: PREPROCESS DEPENDENCIES ---
|
|
@@ -508,5 +510,5 @@ class SBOMEnricher:
|
|
|
508
510
|
"""Save enriched SBOM"""
|
|
509
511
|
with open(output_path, "w", encoding="utf-8") as f:
|
|
510
512
|
json.dump(self.sbom_data, f, indent=2)
|
|
511
|
-
|
|
513
|
+
logging.info(f"Enriched SBOM saved to {output_path}")
|
|
512
514
|
|
|
@@ -19,7 +19,11 @@ import json
|
|
|
19
19
|
import xmltodict
|
|
20
20
|
import yaml
|
|
21
21
|
import time
|
|
22
|
-
|
|
22
|
+
from json import JSONDecodeError
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
23
27
|
logging.basicConfig(format='%(asctime)s — %(name)s — %(levelname)s — %(funcName)s:%(lineno)d — %(message)s')
|
|
24
28
|
|
|
25
29
|
docker = Config.docker_client
|
|
@@ -263,7 +267,7 @@ def showSummary():
|
|
|
263
267
|
logging.info('--------------------------- Scan Token: %s --------------------------------------------------------',Config.scan_token)
|
|
264
268
|
if Config.git_present:
|
|
265
269
|
logging.info('--------------------------- Git Details:[Repo: %s, Branch: %s ]--------------------------------------------------------',Config.git_repo,Config.git_branch)
|
|
266
|
-
logging.info('---------------------------
|
|
270
|
+
#logging.info('--------------------------- Total Lines of Code Scanned: %s --------------------------------------------------------',Config.cloc_total_data)
|
|
267
271
|
# Step 3: Loop through each item in the JSON object
|
|
268
272
|
for file_type, details in Config.lingu_details.items():
|
|
269
273
|
logging.info('--------------------------- %s : %s : %s%%--------------------------------------------------------', file_type, details['size'], details['percentage'])
|
|
@@ -947,13 +951,71 @@ def fetch_zap_advance_config():
|
|
|
947
951
|
exit(1) #server/saas error
|
|
948
952
|
|
|
949
953
|
|
|
954
|
+
### read cloc data
|
|
955
|
+
|
|
956
|
+
def load_cloc_json_safe(path="cloc.json"):
|
|
957
|
+
empty_data = {}
|
|
958
|
+
|
|
959
|
+
try:
|
|
960
|
+
file_path = Path(path)
|
|
961
|
+
|
|
962
|
+
if not file_path.exists():
|
|
963
|
+
return empty_data
|
|
964
|
+
|
|
965
|
+
if file_path.stat().st_size == 0:
|
|
966
|
+
return empty_data
|
|
967
|
+
|
|
968
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
969
|
+
data = json.load(f)
|
|
970
|
+
|
|
971
|
+
# Validate expected structure
|
|
972
|
+
if not isinstance(data, dict):
|
|
973
|
+
return empty_data
|
|
974
|
+
|
|
975
|
+
# Optional: cloc usually contains SUM
|
|
976
|
+
if "SUM" not in data:
|
|
977
|
+
return empty_data
|
|
978
|
+
|
|
979
|
+
Config.cloc_total_data = data['SUM']['code']
|
|
980
|
+
return data
|
|
981
|
+
|
|
982
|
+
except (JSONDecodeError, OSError, ValueError):
|
|
983
|
+
# Any parsing or IO error → empty result
|
|
984
|
+
return empty_data
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
|
|
950
990
|
def uploadLogs():
|
|
951
991
|
|
|
952
992
|
# Get the captured log messages
|
|
953
993
|
all_logs = Config.log_stream.logs
|
|
954
994
|
|
|
955
|
-
#
|
|
995
|
+
# fetching the cloc results
|
|
996
|
+
|
|
997
|
+
# Usage
|
|
998
|
+
cloc_data = load_cloc_json_safe("cloc.json")
|
|
999
|
+
#print(type(cloc_data))
|
|
1000
|
+
# cloc_data is a STRING, not dict
|
|
1001
|
+
#cloc_data = cloc_data.strip()
|
|
1002
|
+
|
|
1003
|
+
# try:
|
|
1004
|
+
# cloc_data = json.loads(cloc_data) #
|
|
1005
|
+
# except json.JSONDecodeError:
|
|
1006
|
+
# cloc_data = {} #
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
if not cloc_data:
|
|
1010
|
+
logging.error("Invalid or empty cloc JSON, using empty data")
|
|
1011
|
+
else:
|
|
1012
|
+
logging.info("Valid cloc JSON loaded")
|
|
956
1013
|
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
#parameter setup
|
|
957
1019
|
body = {
|
|
958
1020
|
'auth_token': Config.app_token,
|
|
959
1021
|
'customer_token': Config.customer_token,
|
|
@@ -962,7 +1024,8 @@ def uploadLogs():
|
|
|
962
1024
|
'git_repo_name':Config.git_repo,
|
|
963
1025
|
'git_branch_name':Config.git_branch,
|
|
964
1026
|
'full_logs':all_logs,
|
|
965
|
-
'lingu_details':Config.lingu_details
|
|
1027
|
+
'lingu_details':Config.lingu_details,
|
|
1028
|
+
'loc_details':cloc_data
|
|
966
1029
|
# Add more parameters if needed
|
|
967
1030
|
}
|
|
968
1031
|
|
|
@@ -972,7 +1035,7 @@ def uploadLogs():
|
|
|
972
1035
|
|
|
973
1036
|
url = Config.boman_url+"/api/scan/logs/upload"
|
|
974
1037
|
|
|
975
|
-
response = requests.post(url,
|
|
1038
|
+
response = requests.post(url, json=body)
|
|
976
1039
|
|
|
977
1040
|
if response.status_code == 200:
|
|
978
1041
|
#logging.info('Uploading logs for the scan token %s',str(Config.scan_token))
|
|
@@ -1141,6 +1204,9 @@ def get_summary_of_vulns(response):
|
|
|
1141
1204
|
Config.medium_count = vuln['MEDIUM']
|
|
1142
1205
|
Config.high_count = vuln['HIGH']
|
|
1143
1206
|
Config.critical_count = vuln['CRITICAL']
|
|
1207
|
+
|
|
1208
|
+
Config.sla_fail_build= response["build_fail"]
|
|
1209
|
+
Config.reason_sla_build_fail = response["reason_for_failing"]
|
|
1144
1210
|
|
|
1145
1211
|
logging.info('Summary: Analyzing Vulnerabitlites Done')
|
|
1146
1212
|
|
|
@@ -1174,3 +1240,5 @@ def ml_start(app_token, customer_token, scan_token):
|
|
|
1174
1240
|
logging.error(f"Connection Error: Can't connect to the Server, Please check your Internet connection. Error: {e}")
|
|
1175
1241
|
else:
|
|
1176
1242
|
logging.warning("ML Start API has been already called....")
|
|
1243
|
+
|
|
1244
|
+
|
|
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
|