inconnu 0.1.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.
@@ -0,0 +1,657 @@
1
+ #!/usr/bin/env python3
2
+ # ruff: noqa: S603
3
+ """
4
+ Create tasks on a Bitbucket pull request based on Corgea security scan results.
5
+
6
+ Usage (inside Bitbucket Pipelines step):
7
+ python .bitbucket/scripts/auto_fix.py corgea_issues/
8
+
9
+ Environment variables used (all provided by Pipelines):
10
+ BITBUCKET_PR_ID – current PR id (empty when not in PR context)
11
+ BITBUCKET_REPO_SLUG – repo slug, e.g. "inconnu"
12
+ BITBUCKET_WORKSPACE – workspace id, e.g. "0xjgv"
13
+ BITBUCKET_ACCESS_TOKEN – access token for authentication
14
+
15
+ The script will create tasks on the PR for security issues found:
16
+ - Individual detailed tasks for each security issue with fix suggestions
17
+ - A summary task with an overview of all security issues found
18
+ """
19
+
20
+ import json
21
+ import os
22
+ import pathlib
23
+ import sys
24
+
25
+ import requests
26
+
27
+ __all__ = ["main"]
28
+
29
+ BB_API_ROOT = "https://api.bitbucket.org/2.0"
30
+
31
+
32
+ class ConfigError(RuntimeError):
33
+ """Raised when required environment variables are missing."""
34
+
35
+
36
+ def _env(key: str) -> str:
37
+ try:
38
+ return os.environ[key]
39
+ except KeyError as exc:
40
+ raise ConfigError(f"Missing required env-var: {key}") from exc
41
+
42
+
43
+ def _create_summary_task(pr_id: str, message: str) -> bool:
44
+ """Create a summary task on a pull request for all security issues.
45
+
46
+ Returns True if successful, False otherwise.
47
+ """
48
+ access_token = os.environ.get("BITBUCKET_ACCESS_TOKEN")
49
+ if not access_token:
50
+ sys.stderr.write(
51
+ "Error: BITBUCKET_ACCESS_TOKEN environment variable not set.\n"
52
+ )
53
+ return False
54
+
55
+ url = (
56
+ f"{BB_API_ROOT}/repositories/"
57
+ f"{_env('BITBUCKET_WORKSPACE')}/"
58
+ f"{_env('BITBUCKET_REPO_SLUG')}/pullrequests/{pr_id}/tasks"
59
+ )
60
+
61
+ payload = {"content": {"raw": message, "markup": "markdown"}, "pending": True}
62
+
63
+ headers = {
64
+ "Authorization": f"Bearer {access_token}",
65
+ "Content-Type": "application/json",
66
+ }
67
+
68
+ try:
69
+ resp = requests.post(url, json=payload, headers=headers, timeout=10)
70
+ resp.raise_for_status()
71
+ return True
72
+ except requests.HTTPError as err:
73
+ sys.stderr.write(f"Failed to create summary task on PR {pr_id}: {err}\n")
74
+ if hasattr(resp, "text"):
75
+ sys.stderr.write(f"Response: {resp.text}\n")
76
+ return False
77
+ except Exception as exc:
78
+ sys.stderr.write(f"Error creating summary task: {exc}\n")
79
+ return False
80
+
81
+
82
+ def _create_inline_comment(
83
+ pr_id: str, content: str, file_path: str, line_number: int
84
+ ) -> int | None:
85
+ """Create an inline comment on a pull request.
86
+
87
+ Args:
88
+ pr_id: Pull request ID
89
+ content: Comment content
90
+ file_path: File path for the inline comment
91
+ line_number: Line number for the inline comment
92
+
93
+ Returns comment ID if successful, None otherwise.
94
+ """
95
+ access_token = os.environ.get("BITBUCKET_ACCESS_TOKEN")
96
+ if not access_token:
97
+ sys.stderr.write(
98
+ "Error: BITBUCKET_ACCESS_TOKEN environment variable not set.\n"
99
+ )
100
+ return None
101
+
102
+ url = (
103
+ f"{BB_API_ROOT}/repositories/"
104
+ f"{_env('BITBUCKET_WORKSPACE')}/"
105
+ f"{_env('BITBUCKET_REPO_SLUG')}/pullrequests/{pr_id}/comments"
106
+ )
107
+
108
+ payload = {
109
+ "content": {"raw": content},
110
+ "inline": {"from": line_number, "to": line_number, "path": file_path},
111
+ }
112
+
113
+ headers = {
114
+ "Authorization": f"Bearer {access_token}",
115
+ "Content-Type": "application/json",
116
+ }
117
+
118
+ try:
119
+ resp = requests.post(url, json=payload, headers=headers, timeout=10)
120
+ resp.raise_for_status()
121
+ comment_data = resp.json()
122
+ return comment_data.get("id")
123
+ except requests.HTTPError as err:
124
+ sys.stderr.write(f"Failed to create inline comment on PR {pr_id}: {err}\n")
125
+ if hasattr(resp, "text"):
126
+ sys.stderr.write(f"Response: {resp.text}\n")
127
+ return None
128
+ except Exception as exc:
129
+ sys.stderr.write(f"Error creating inline comment: {exc}\n")
130
+ return None
131
+
132
+
133
+ def _create_task(pr_id: str, content: str, comment_id: int | None = None) -> bool:
134
+ """Create a task on a pull request, optionally linked to a comment.
135
+
136
+ Args:
137
+ pr_id: Pull request ID
138
+ content: Task content
139
+ comment_id: Optional comment ID to link the task to
140
+
141
+ Returns True if successful, False otherwise.
142
+ """
143
+ access_token = os.environ.get("BITBUCKET_ACCESS_TOKEN")
144
+ if not access_token:
145
+ sys.stderr.write(
146
+ "Error: BITBUCKET_ACCESS_TOKEN environment variable not set.\n"
147
+ )
148
+ return False
149
+
150
+ url = (
151
+ f"{BB_API_ROOT}/repositories/"
152
+ f"{_env('BITBUCKET_WORKSPACE')}/"
153
+ f"{_env('BITBUCKET_REPO_SLUG')}/pullrequests/{pr_id}/tasks"
154
+ )
155
+
156
+ payload = {"content": {"raw": content, "markup": "markdown"}, "pending": True}
157
+
158
+ # Link to comment if provided
159
+ if comment_id:
160
+ payload["comment"] = {"id": comment_id}
161
+
162
+ headers = {
163
+ "Authorization": f"Bearer {access_token}",
164
+ "Content-Type": "application/json",
165
+ }
166
+
167
+ try:
168
+ resp = requests.post(url, json=payload, headers=headers, timeout=10)
169
+ resp.raise_for_status()
170
+ return True
171
+ except requests.HTTPError as err:
172
+ sys.stderr.write(f"Failed to create task on PR {pr_id}: {err}\n")
173
+ if hasattr(resp, "text"):
174
+ sys.stderr.write(f"Response: {resp.text}\n")
175
+ return False
176
+ except Exception as exc:
177
+ sys.stderr.write(f"Error creating task: {exc}\n")
178
+ return False
179
+
180
+
181
+ def _create_comment_and_task(pr_id: str, issue: dict) -> bool:
182
+ """Create an inline comment and linked task for a security issue.
183
+
184
+ Args:
185
+ pr_id: Pull request ID
186
+ issue: Dict containing comprehensive issue information
187
+
188
+ Returns True if both comment and task created successfully, False otherwise.
189
+ """
190
+ # Step 1: Create inline comment with concise issue details
191
+ comment_content = _build_inline_comment_content(issue)
192
+ comment_id = _create_inline_comment(
193
+ pr_id, comment_content, issue["file"], issue["line"]
194
+ )
195
+
196
+ if not comment_id:
197
+ # Fallback: create standalone task if comment creation fails
198
+ sys.stderr.write(
199
+ f"Failed to create inline comment for issue {issue['issue_id']}, creating standalone task\n"
200
+ )
201
+ task_content = _build_task_content(issue)
202
+ return _create_task(pr_id, task_content)
203
+
204
+ # Step 2: Create detailed task linked to the comment
205
+ task_content = _build_task_content(issue)
206
+ return _create_task(pr_id, task_content, comment_id)
207
+
208
+
209
+ def _extract_issue_summary(issue: dict) -> dict:
210
+ """Extract key information from a security issue.
211
+
212
+ Returns a dict with file, line, title, and urgency.
213
+ """
214
+ # Extract location information
215
+ location = issue.get("location", {})
216
+ file_info = location.get("file", {})
217
+ file_path = (
218
+ file_info.get("path") or location.get("file") or issue.get("file") or "unknown"
219
+ )
220
+ line_no = (
221
+ location.get("line_number")
222
+ or location.get("line")
223
+ or location.get("endLine")
224
+ or 0
225
+ )
226
+
227
+ # Extract classification details
228
+ classification = issue.get("classification", {})
229
+ title = (
230
+ classification.get("name")
231
+ or issue.get("title")
232
+ or issue.get("ruleId", "Security Issue")
233
+ )
234
+
235
+ # Extract urgency
236
+ urgency = issue.get("urgency", "UNKNOWN")
237
+
238
+ return {"file": file_path, "line": line_no, "title": title, "urgency": urgency}
239
+
240
+
241
+ def _extract_full_issue(issue: dict) -> dict:
242
+ """Extract comprehensive information from a security issue for task creation.
243
+
244
+ Returns a dict with all relevant issue information including auto-fix suggestions.
245
+ """
246
+ # Get the basic summary first
247
+ summary = _extract_issue_summary(issue)
248
+
249
+ # Extract additional detailed information
250
+ classification = issue.get("classification", {})
251
+ details = issue.get("details", {})
252
+ auto_fix = issue.get("auto_fix_suggestion", {})
253
+ location = issue.get("location", {})
254
+ file_info = location.get("file", {})
255
+ project_info = location.get("project", {})
256
+ auto_triage = issue.get("auto_triage", {})
257
+ false_positive = auto_triage.get("false_positive_detection", {})
258
+
259
+ # Build comprehensive issue data
260
+ full_issue = {
261
+ **summary,
262
+ "issue_id": issue.get("id", "unknown"),
263
+ "scan_id": issue.get("scan_id", ""),
264
+ "status": issue.get("status", ""),
265
+ "created_at": issue.get("created_at", ""),
266
+ "description": classification.get("description", ""),
267
+ "explanation": details.get("explanation", ""),
268
+ "cwe_id": classification.get("id", ""),
269
+ "file_name": file_info.get("name", ""),
270
+ "file_language": file_info.get("language", ""),
271
+ "project_name": project_info.get("name", ""),
272
+ "project_branch": project_info.get("branch", ""),
273
+ "git_sha": project_info.get("git_sha", ""),
274
+ "triage_status": false_positive.get("status", ""),
275
+ "triage_reasoning": false_positive.get("reasoning", ""),
276
+ "has_auto_fix": auto_fix.get("status") == "fix_available",
277
+ }
278
+
279
+ # Add auto-fix information if available
280
+ if full_issue["has_auto_fix"]:
281
+ patch = auto_fix.get("patch", {})
282
+ full_issue.update(
283
+ {
284
+ "fix_id": auto_fix.get("id", ""),
285
+ "fix_explanation": patch.get("explanation", ""),
286
+ "fix_diff": patch.get("diff", ""),
287
+ }
288
+ )
289
+
290
+ return full_issue
291
+
292
+
293
+ def _build_inline_comment_content(issue: dict) -> str:
294
+ """Build concise inline comment content for diff view.
295
+
296
+ Args:
297
+ issue: Dict containing comprehensive issue information from _extract_full_issue
298
+
299
+ Returns:
300
+ Formatted inline comment content string
301
+ """
302
+ urgency_labels = {"CR": "🔴", "HI": "🟠", "ME": "🟡", "LO": "🟢"}
303
+ urgency_emoji = urgency_labels.get(issue["urgency"], "⚪")
304
+
305
+ parts = [
306
+ f"## {urgency_emoji} Security Issue: {issue['title']}",
307
+ "",
308
+ f"**{issue['cwe_id']}** - {issue.get('description', 'Security vulnerability detected')}",
309
+ "",
310
+ ]
311
+
312
+ # Add collapsible detailed explanation
313
+ if issue.get("explanation"):
314
+ # Clean up HTML for better markdown display
315
+ explanation = (
316
+ issue["explanation"].replace("<br><br>", "\n\n").replace("<br>", "\n")
317
+ )
318
+ explanation = explanation.replace("<code>", "`").replace("</code>", "`")
319
+
320
+ parts.extend(
321
+ [
322
+ "---",
323
+ "",
324
+ "### 📋 Detailed Explanation",
325
+ "",
326
+ explanation,
327
+ "",
328
+ ]
329
+ )
330
+
331
+ # Add fix if available
332
+ if (
333
+ issue.get("has_auto_fix")
334
+ and issue.get("fix_diff")
335
+ and issue["fix_diff"].strip()
336
+ ):
337
+ # Clean up fix explanation HTML
338
+ fix_explanation = issue.get("fix_explanation", "Auto-fix available")
339
+ fix_explanation = fix_explanation.replace("<br><br>", "\n\n").replace(
340
+ "<br>", "\n"
341
+ )
342
+ fix_explanation = fix_explanation.replace("<code>", "`").replace("</code>", "`")
343
+ # Handle list items - add newline before each item except the first
344
+ fix_explanation = fix_explanation.replace("</li><li>", "\n- ")
345
+ fix_explanation = fix_explanation.replace("<li>", "- ").replace("</li>", "")
346
+ fix_explanation = fix_explanation.replace("<ul>", "").replace("</ul>", "")
347
+ fix_explanation = fix_explanation.replace("<ol>", "").replace("</ol>", "")
348
+ # Strip leading/trailing whitespace and ensure proper line breaks
349
+ fix_explanation = fix_explanation.strip()
350
+
351
+ parts.extend(
352
+ [
353
+ "---",
354
+ "",
355
+ "### 🔧 Suggested Fix",
356
+ "",
357
+ "**Fix explanation:**",
358
+ "",
359
+ fix_explanation,
360
+ "\n",
361
+ "**📊 Changes (diff view):**",
362
+ "```diff",
363
+ issue["fix_diff"].strip(),
364
+ "```",
365
+ "",
366
+ "**📥 Apply this fix:**",
367
+ "\n",
368
+ f"1. [Download patch file](https://www.corgea.app/issue/fix-diff/{issue['issue_id']}?download=true)",
369
+ "\n",
370
+ "2. Apply it: `git apply corgea-fix-{}.patch`".format(
371
+ issue["issue_id"][:8]
372
+ ),
373
+ "\n",
374
+ ]
375
+ )
376
+
377
+ parts.extend(
378
+ [f"🔗 **[View on Corgea](https://www.corgea.app/issue/{issue['issue_id']}/)**"]
379
+ )
380
+
381
+ return "\n".join(filter(None, parts))
382
+
383
+
384
+ def _build_task_content(issue: dict) -> str:
385
+ """Build task content from a detailed security issue.
386
+
387
+ Args:
388
+ issue: Dict containing comprehensive issue information from _extract_full_issue
389
+
390
+ Returns:
391
+ Formatted task content string
392
+ """
393
+ urgency_labels = {
394
+ "CR": "🔴 Critical",
395
+ "HI": "🟠 High",
396
+ "ME": "🟡 Medium",
397
+ "LO": "🟢 Low",
398
+ }
399
+ urgency_label = urgency_labels.get(issue["urgency"], f"⚪ {issue['urgency']}")
400
+
401
+ content_parts = [
402
+ f"## {urgency_label}: {issue['title']}",
403
+ "",
404
+ f"**File:** `{issue['file']}` (line {issue['line']})",
405
+ f"**CWE:** {issue['cwe_id']}" if issue["cwe_id"] else "",
406
+ f"**Language:** {issue['file_language']}" if issue.get("file_language") else "",
407
+ f"**Issue ID:** `{issue['issue_id']}`",
408
+ f"**Scan ID:** `{issue['scan_id']}`" if issue.get("scan_id") else "",
409
+ "",
410
+ ]
411
+
412
+ # Add project context if available
413
+ project_info = []
414
+ if issue.get("project_name"):
415
+ project_info.append(f"**Project:** {issue['project_name']}")
416
+ if issue.get("project_branch"):
417
+ project_info.append(f"**Branch:** {issue['project_branch']}")
418
+ if issue.get("git_sha"):
419
+ project_info.append(f"**Commit:** `{issue['git_sha'][:8]}...`")
420
+ if issue.get("created_at"):
421
+ project_info.append(f"**Detected:** {issue['created_at']}")
422
+
423
+ if project_info:
424
+ content_parts.extend(project_info)
425
+ content_parts.append("")
426
+
427
+ # Add triage information if available
428
+ if issue.get("triage_status"):
429
+ triage_emoji = "✅" if issue["triage_status"] == "valid" else "⚠️"
430
+ content_parts.extend(
431
+ [
432
+ f"### {triage_emoji} Auto-Triage: {issue['triage_status'].title()}",
433
+ issue.get("triage_reasoning", ""),
434
+ "",
435
+ ]
436
+ )
437
+
438
+ # Add description if available
439
+ if issue.get("description"):
440
+ content_parts.extend(
441
+ [
442
+ "### Description",
443
+ issue["description"],
444
+ "",
445
+ ]
446
+ )
447
+
448
+ # Add detailed explanation if available
449
+ if issue.get("explanation"):
450
+ content_parts.extend(
451
+ [
452
+ "### Technical Details",
453
+ issue["explanation"],
454
+ "",
455
+ ]
456
+ )
457
+
458
+ # Add auto-fix suggestion if available
459
+ if issue.get("has_auto_fix"):
460
+ content_parts.extend(
461
+ [
462
+ "### 🔧 Suggested Fix",
463
+ issue.get("fix_explanation", "Auto-fix available"),
464
+ "",
465
+ ]
466
+ )
467
+
468
+ if issue.get("fix_id"):
469
+ content_parts.extend(
470
+ [
471
+ f"**Fix ID:** `{issue['fix_id']}`",
472
+ "",
473
+ ]
474
+ )
475
+
476
+ if issue.get("fix_diff") and issue["fix_diff"].strip():
477
+ # Use language-specific syntax highlighting if available, otherwise default to diff
478
+ diff_language = issue.get("file_language", "diff")
479
+ content_parts.extend(
480
+ [
481
+ "### Proposed Changes",
482
+ f"```{diff_language}",
483
+ issue["fix_diff"].strip(),
484
+ "```",
485
+ "",
486
+ ]
487
+ )
488
+
489
+ content_parts.extend(
490
+ [
491
+ "---",
492
+ "**Action Required:** Review and apply the necessary security fixes for this issue.",
493
+ "",
494
+ f"🔗 **[View Full Issue Details on Corgea](https://www.corgea.app/issue/{issue['issue_id']}/)**",
495
+ "",
496
+ "*Generated by Corgea Security Scan*",
497
+ ]
498
+ )
499
+
500
+ return "\n".join(filter(None, content_parts))
501
+
502
+
503
+ def _iter_issue_files(dir_path: pathlib.Path):
504
+ """Iterate over JSON issue files in the directory."""
505
+ for json_file in dir_path.glob("*.json"):
506
+ try:
507
+ data = json.loads(json_file.read_text())
508
+ # Handle both wrapped {"issue": {...}} and direct issue format
509
+ issue = data.get("issue", data) if isinstance(data, dict) else data
510
+ yield issue
511
+ except json.JSONDecodeError as exc:
512
+ sys.stderr.write(f"Skipping invalid JSON {json_file}: {exc}\n")
513
+
514
+
515
+ def _build_summary_task_message(issues: list[dict]) -> str:
516
+ """Build a comprehensive summary task message from security issues."""
517
+ if not issues:
518
+ return ""
519
+
520
+ # Group issues by urgency
521
+ by_urgency = {}
522
+ for issue in issues:
523
+ urgency = issue["urgency"]
524
+ if urgency not in by_urgency:
525
+ by_urgency[urgency] = []
526
+ by_urgency[urgency].append(issue)
527
+
528
+ # Build message
529
+ parts = [
530
+ "🔒 **Security Issues Found**",
531
+ "",
532
+ f"Corgea security scan identified **{len(issues)} security issue(s)** that need to be addressed before this PR can be merged.",
533
+ "",
534
+ ]
535
+
536
+ # Add urgency-based sections
537
+ urgency_order = ["CR", "HI", "ME", "LO"] # Critical, High, Medium, Low
538
+ urgency_labels = {"CR": "Critical", "HI": "High", "ME": "Medium", "LO": "Low"}
539
+
540
+ for urgency in urgency_order:
541
+ if urgency in by_urgency:
542
+ urgency_issues = by_urgency[urgency]
543
+ urgency_label = urgency_labels.get(urgency, urgency)
544
+
545
+ parts.extend(
546
+ [f"### {urgency_label} Priority ({len(urgency_issues)} issue(s))", ""]
547
+ )
548
+
549
+ for issue in urgency_issues:
550
+ parts.append(
551
+ f"- **{issue['title']}** in `{issue['file']}` (line {issue['line']})"
552
+ )
553
+
554
+ parts.append("")
555
+
556
+ # Add footer
557
+ parts.extend(
558
+ [
559
+ "---",
560
+ "**Next Steps:**",
561
+ "1. Review the security issues identified above",
562
+ "2. Check the individual tasks created for each issue with detailed fix suggestions",
563
+ "3. Apply the necessary fixes to your code",
564
+ "4. Push your changes to update this pull request",
565
+ "5. The security scan will automatically re-run to verify fixes",
566
+ "",
567
+ "*This summary task was automatically generated by Corgea security scanning.*",
568
+ ]
569
+ )
570
+
571
+ return "\n".join(parts)
572
+
573
+
574
+ def main(directory: str = "corgea_issues") -> None: # pragma: no cover
575
+ pr_id = os.environ.get("BITBUCKET_PR_ID")
576
+ if not pr_id:
577
+ sys.stderr.write("No BITBUCKET_PR_ID – not in a PR context, exiting.\n")
578
+ return
579
+
580
+ path = pathlib.Path(directory)
581
+ if not path.is_dir():
582
+ raise SystemExit(f"Artifact directory '{directory}' not found")
583
+
584
+ # Collect all security issues with full details
585
+ issues = []
586
+ full_issues = []
587
+ for issue_data in _iter_issue_files(path):
588
+ try:
589
+ issue_summary = _extract_issue_summary(issue_data)
590
+ full_issue = _extract_full_issue(issue_data)
591
+ issues.append(issue_summary)
592
+ full_issues.append(full_issue)
593
+ except Exception as exc: # noqa: BLE001
594
+ sys.stderr.write(f"Error parsing issue: {exc}\n")
595
+ continue
596
+
597
+ print(f"Found {len(issues)} security issue(s) from {directory}")
598
+
599
+ if issues:
600
+ # Create individual inline comments and tasks for each issue
601
+ task_success_count = 0
602
+ comment_success_count = 0
603
+ for full_issue in full_issues:
604
+ if _create_comment_and_task(pr_id, full_issue):
605
+ task_success_count += 1
606
+ # Check if it has valid file/line for inline comment
607
+ if (
608
+ full_issue.get("file")
609
+ and full_issue.get("line")
610
+ and full_issue["line"] > 0
611
+ ):
612
+ comment_success_count += 1
613
+ else:
614
+ sys.stderr.write(
615
+ f"Failed to create comment and task for issue {full_issue['issue_id']}\n"
616
+ )
617
+
618
+ if comment_success_count > 0:
619
+ print(
620
+ f"✅ Created {comment_success_count} inline comments with linked tasks and {task_success_count - comment_success_count} standalone tasks ({task_success_count}/{len(full_issues)} total)"
621
+ )
622
+ else:
623
+ print(
624
+ f"✅ Created {task_success_count}/{len(full_issues)} standalone tasks for individual issues"
625
+ )
626
+
627
+ # Security issues found - create summary task
628
+ message = _build_summary_task_message(issues)
629
+ if _create_summary_task(pr_id, message):
630
+ print(
631
+ f"✅ Created summary task on PR #{pr_id} for {len(issues)} security issue(s)"
632
+ )
633
+
634
+ # Print summary by urgency
635
+ by_urgency = {}
636
+ for issue in issues:
637
+ urgency = issue["urgency"]
638
+ by_urgency[urgency] = by_urgency.get(urgency, 0) + 1
639
+
640
+ print("\nIssue breakdown:")
641
+ for urgency, count in sorted(by_urgency.items()):
642
+ print(f" {urgency}: {count}")
643
+ else:
644
+ print(f"❌ Failed to create summary task on PR #{pr_id}")
645
+ sys.exit(1)
646
+ else:
647
+ # No security issues found
648
+ print("✅ No security issues found - no tasks created")
649
+
650
+
651
+ if __name__ == "__main__":
652
+ dir_arg = sys.argv[1] if len(sys.argv) > 1 else "corgea_issues"
653
+ try:
654
+ main(dir_arg)
655
+ except ConfigError as ce:
656
+ sys.stderr.write(str(ce) + "\n")
657
+ sys.exit(1)