├── .github
├── ISSUE_TEMPLATE
│ ├── bug-report.yml
│ ├── config.yml
│ └── feature-request.yml
├── scripts
│ ├── check_and_install_stub.py
│ └── scan_org_licenses_licensee.py
└── workflows
│ ├── dependency-scan.yml
│ ├── license-cla-check.yml
│ ├── manage-cla-stubs.yml
│ ├── old
│ └── license-cal-check.yml
│ ├── python_licensee_scan.yml
│ ├── reusable-cla-check.yml
│ ├── scan-licenses.yml
│ └── secrets-scanning-report.yml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── SECURITY.md
├── actions
└── check-license
│ └── action.yml
├── permissive_licenses.json
├── profile
├── README.md
└── image1.jpg
├── reports
├── secret_report_20250217_002349.csv
├── secret_report_20250224_002324.csv
├── secret_report_20250303_002412.csv
├── secret_report_20250310_002003.csv
├── secret_report_20250317_002435.csv
├── secret_report_20250324_002442.csv
├── secret_report_20250331_002538.csv
├── secret_report_20250407_002459.csv
├── secret_report_20250414_002556.csv
├── secret_report_20250421_002953.csv
├── secret_report_20250428_002604.csv
├── vulnerability_report_20250218_185722.csv
├── vulnerability_report_20250224_000546.csv
├── vulnerability_report_20250303_000551.csv
├── vulnerability_report_20250310_000510.csv
├── vulnerability_report_20250317_000607.csv
├── vulnerability_report_20250324_000606.csv
├── vulnerability_report_20250331_000603.csv
├── vulnerability_report_20250407_000559.csv
├── vulnerability_report_20250414_000614.csv
├── vulnerability_report_20250421_000559.csv
└── vulnerability_report_20250428_000622.csv
├── scripts
├── check_licenses.py
├── dependency_scanner.py
├── github_secret_scanner.py
└── requirements.txt
├── signatures
└── CLA.json
└── templates
└── cla-trigger-template.yml
/.github/ISSUE_TEMPLATE/bug-report.yml:
--------------------------------------------------------------------------------
1 | name: 🐛 Bug report
2 | description: Create a report to help us improve
3 | labels: bug
4 |
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Thanks for taking the time to fill out this bug report!
10 |
11 | - type: textarea
12 | id: describe-bug
13 | attributes:
14 | label: Describe the bug
15 | description: A clear and concise description of what the bug is.
16 | validations:
17 | required: true
18 |
19 | - type: textarea
20 | id: reproduction-steps
21 | attributes:
22 | label: Reproduction steps
23 | description: Steps to reproduce the behavior
24 | value: |
25 | 1.
26 | 2.
27 | 3.
28 | ...
29 | validations:
30 | required: true
31 |
32 | - type: textarea
33 | id: expected-behavior
34 | attributes:
35 | label: Expected behavior
36 | description: A clear and concise description of what you expected to happen.
37 | validations:
38 | required: true
39 |
40 | - type: textarea
41 | id: additional-context
42 | attributes:
43 | label: Additional context
44 | description: Add any other context about the problem here.
45 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature-request.yml:
--------------------------------------------------------------------------------
1 | name: 🚀 Feature request
2 | description: Suggest an idea for this project
3 | labels: enhancement
4 |
5 | body:
6 | - type: textarea
7 | id: describe-problem
8 | attributes:
9 | label: Is your feature request related to a problem? Please describe.
10 | description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 | validations:
12 | required: true
13 |
14 | - type: textarea
15 | id: describe-solution
16 | attributes:
17 | label: Describe the solution you'd like
18 | description: A clear and concise description of what you want to happen.
19 | validations:
20 | required: true
21 |
22 | - type: textarea
23 | id: describe-alternatives
24 | attributes:
25 | label: Describe alternatives you've considered
26 | description: A clear and concise description of any alternative solutions or features you've considered.
27 | validations:
28 | required: false
29 |
30 | - type: textarea
31 | id: additional-context
32 | attributes:
33 | label: Additional context
34 | description: Add any other context or screenshots about the feature request here.
35 | validations:
36 | required: false
37 |
--------------------------------------------------------------------------------
/.github/scripts/check_and_install_stub.py:
--------------------------------------------------------------------------------
1 | # File: .github/scripts/check_and_install_stub.py
2 | # Purpose: Determines a repository's license, classifies it, and manages a CLA trigger workflow stub.
3 | # The stub now triggers on both pull_request_target and issue_comment events.
4 |
5 | import os
6 | import json
7 | import subprocess
8 | import base64
9 | import time
10 | import logging
11 | from github import Github, GithubException, UnknownObjectException
12 |
13 | # --- Configuration ---
14 | ORG_NAME = os.environ.get("GITHUB_REPOSITORY_OWNER")
15 | # IMPORTANT: Increment this version due to changes in the stub template's trigger logic.
16 | TARGET_STUB_VERSION = "1.1.0" # Example: Major.Minor.Patch -> new trigger is significant
17 | STUB_WORKFLOW_PATH = ".github/workflows/cla-check-trigger.yml"
18 |
19 | # Get the Licensee Docker image tag from an environment variable set by the workflow.
20 | LICENSEE_DOCKER_IMAGE_TAG = os.environ.get("LICENSEE_DOCKER_IMAGE", "local-org-licensee:latest")
21 |
22 | if not ORG_NAME:
23 | logging.critical("CRITICAL: GITHUB_REPOSITORY_OWNER environment variable not set. Cannot proceed.")
24 | exit(1)
25 |
26 | # Default URL to your CLA document stored within the .github repository itself.
27 | # This will be embedded in the stub workflow.
28 | DEFAULT_CLA_DOCUMENT_URL_IN_STUB = f"https://github.com/{ORG_NAME}/.github/blob/main/.github/CONTRIBUTOR_LICENSE_AGREEMENT.md"
29 | # If you prefer to configure this via an environment variable from manage-cla-stubs.yml:
30 | # CLA_DOCUMENT_URL_FOR_STUB_FINAL = os.environ.get("CLA_DOCUMENT_URL_FOR_STUBS_ENV_VAR", DEFAULT_CLA_DOCUMENT_URL_IN_STUB)
31 |
32 |
33 | # Define the content of the stub workflow file.
34 | # This stub triggers the reusable workflow for both PR events and relevant PR comments.
35 | STUB_WORKFLOW_CONTENT_TEMPLATE = f"""\
36 | # This file is auto-generated and managed by the organization's .github repository.
37 | # Do not modify manually. Version: {TARGET_STUB_VERSION}
38 | name: CLA Check Trigger
39 |
40 | on:
41 | # Trigger on pull request events (opened, new commits pushed, reopened)
42 | pull_request_target:
43 | types: [opened, synchronize, reopened]
44 | # Trigger on issue comments (pull requests are also 'issues' in GitHub's model)
45 | issue_comment:
46 | types: [created] # Only when a new comment is made
47 |
48 | jobs:
49 | call_cla_check:
50 | # This job will run if:
51 | # 1. The event is 'pull_request_target'.
52 | # OR
53 | # 2. The event is 'issue_comment' AND the comment was made on a pull request.
54 | # The 'contributor-assistant/github-action' in the reusable workflow will then
55 | # determine if the comment body is relevant for CLA signing or rechecking.
56 | if: >
57 | github.event_name == 'pull_request_target' ||
58 | (github.event_name == 'issue_comment' && github.event.issue.pull_request)
59 |
60 | # Call the organization's centralized reusable CLA checking workflow.
61 | # Pinning to a specific versioned tag (e.g., @v1.0.0) or commit SHA of the reusable workflow is highly recommended for stability.
62 | uses: {ORG_NAME}/.github/.github/workflows/reusable-cla-check.yml@main
63 | secrets:
64 | # Pass the PAT required by contributor-assistant. This PAT needs permissions for:
65 | # - PR interactions (comments, labels, statuses) on THIS repository (where the stub runs).
66 | # - Contents Read & Write on the {ORG_NAME}/.github repository to manage the CLA.csv signature file.
67 | CONTRIBUTOR_ASSISTANT_PAT: ${{{{ secrets.CLA_ASSISTANT_PAT }}}} # Note: Secret name is still CLA_ASSISTANT_PAT as per previous setup
68 | # Can be renamed if desired, but ensure consistency.
69 | with:
70 | # Provide the URL to the CLA document.
71 | cla_document_url: {DEFAULT_CLA_DOCUMENT_URL_IN_STUB} # Using the default determined in Python script
72 | # Optional overrides for signature file path and branch if defaults in reusable workflow are not suitable:
73 | # signature_file_path: '.github/signatures/CLA.csv'
74 | # signature_branch: 'main'
75 | """
76 |
77 | # --- Logging Setup ---
78 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
79 |
80 | # --- get_license_info function ---
81 | # (This function remains unchanged from the previous correct version that builds licensee locally)
82 | def get_license_info(repo_full_name, gh_token, temp_base_dir="temp_license_check"):
83 | g = Github(gh_token)
84 | try:
85 | repo = g.get_repo(repo_full_name)
86 | except UnknownObjectException:
87 | logging.warning(f"Repository {repo_full_name} not found or PAT lacks access.")
88 | return "REPO_NOT_FOUND", "non-permissive"
89 | except Exception as e:
90 | logging.error(f"Error accessing repository {repo_full_name} object: {e}")
91 | return "REPO_ACCESS_ERROR", "non-permissive"
92 |
93 | license_content = None; license_filename = "LICENSE"
94 | common_license_files = ["LICENSE", "LICENSE.MD", "LICENSE.TXT", "COPYING", "COPYING.MD", "UNLICENSE"]
95 | try:
96 | contents = repo.get_contents("")
97 | for content_file in contents:
98 | if content_file.name.upper() in common_license_files:
99 | license_filename = content_file.name
100 | license_content_b64 = repo.get_contents(content_file.path).content
101 | license_content = base64.b64decode(license_content_b64).decode('utf-8', errors='replace')
102 | logging.info(f"Found license file '{content_file.path}' in {repo_full_name}.")
103 | break
104 | except Exception as e:
105 | logging.warning(f"Could not list root contents or read license file from root for {repo_full_name}: {e}. Will try specific paths.")
106 | for fname in common_license_files:
107 | try:
108 | license_content_b64 = repo.get_contents(fname).content
109 | license_content = base64.b64decode(license_content_b64).decode('utf-8', errors='replace')
110 | license_filename = fname; logging.info(f"Found license file '{fname}' directly in {repo_full_name}."); break
111 | except UnknownObjectException: continue
112 | except Exception as e_inner: logging.warning(f"Error fetching specific license file {fname} for {repo_full_name}: {e_inner}"); continue
113 | if not license_content:
114 | logging.info(f"No common license file found for {repo_full_name} via API. Classifying as non-permissive.")
115 | return "NO_LICENSE_FILE", "non-permissive"
116 |
117 | repo_temp_dir = os.path.join(temp_base_dir, repo_full_name.replace("/", "_"))
118 | os.makedirs(repo_temp_dir, exist_ok=True)
119 | temp_license_filepath = os.path.join(repo_temp_dir, license_filename)
120 | try:
121 | with open(temp_license_filepath, "w", encoding="utf-8") as f: f.write(license_content)
122 | cmd = [ "docker", "run", "--rm", "-v", f"{os.path.abspath(repo_temp_dir)}:/scan_dir", LICENSEE_DOCKER_IMAGE_TAG, "detect", "/scan_dir", "--json" ]
123 | logging.info(f"Running licensee for {repo_full_name} using image {LICENSEE_DOCKER_IMAGE_TAG}: {' '.join(cmd)}")
124 | result = subprocess.run(cmd, capture_output=True, text=True, check=False, timeout=90)
125 | if result.returncode != 0:
126 | if "command not found" in result.stderr.lower() or "No such file or directory" in result.stderr.lower():
127 | logging.error(f"Licensee Docker command failed for {repo_full_name}. Docker image '{LICENSEE_DOCKER_IMAGE_TAG}' might not be available or command inside is wrong. Stderr: {result.stderr[:500]}")
128 | else: logging.error(f"Licensee Docker command failed for {repo_full_name}. Exit: {result.returncode}, Stderr: {result.stderr[:500]}")
129 | return "LICENSEE_ERROR", "non-permissive"
130 | license_data = json.loads(result.stdout); spdx_id = "OTHER"
131 | if license_data.get("licenses") and isinstance(license_data["licenses"], list) and license_data["licenses"]:
132 | best_license = max(license_data["licenses"], key=lambda lic: lic.get("confidence", 0), default=None)
133 | if best_license and best_license.get("spdx_id"): spdx_id = best_license["spdx_id"]
134 | elif license_data["licenses"][0].get("spdx_id"): spdx_id = license_data["licenses"][0].get("spdx_id", "OTHER")
135 | elif license_data.get("matched_files") and license_data["matched_files"][0].get("license") and license_data["matched_files"][0]["license"].get("spdx_id"):
136 | spdx_id = license_data["matched_files"][0]["license"]["spdx_id"]
137 | logging.info(f"License for {repo_full_name} determined by licensee as: {spdx_id}")
138 | return spdx_id, classify_license(spdx_id)
139 | except subprocess.TimeoutExpired: logging.error(f"Licensee Docker command timed out for {repo_full_name}."); return "LICENSEE_TIMEOUT", "non-permissive"
140 | except json.JSONDecodeError as e: logging.error(f"Failed to parse licensee JSON for {repo_full_name}. Output: {result.stdout[:300]}. Error: {e}"); return "LICENSEE_JSON_ERROR", "non-permissive"
141 | except Exception as e: logging.error(f"Unexpected error during licensee processing for {repo_full_name}: {e}"); return "UNKNOWN_ERROR_LICENSEE_PROCESSING", "non-permissive"
142 | finally:
143 | if os.path.exists(temp_license_filepath): os.remove(temp_license_filepath)
144 | if os.path.exists(repo_temp_dir) and not os.listdir(repo_temp_dir): os.rmdir(repo_temp_dir)
145 | if os.path.exists(temp_base_dir) and not os.listdir(temp_base_dir):
146 | try: os.rmdir(temp_base_dir)
147 | except OSError: pass
148 |
149 | # --- classify_license function ---
150 | # (This function remains unchanged)
151 | def classify_license(spdx_id):
152 | permissive_spdx_ids_str = os.environ.get("PERMISSIVE_SPDX_IDS", "MIT,Apache-2.0,BSD-3-Clause,ISC,BSD-2-Clause,CC0-1.0,Unlicense")
153 | permissive_ids = {pid.strip().upper() for pid in permissive_spdx_ids_str.split(',')}
154 | if spdx_id is None: logging.warning("classify_license received None SPDX ID, defaulting to non-permissive."); return "non-permissive"
155 | if spdx_id.upper() in permissive_ids: return "permissive"
156 | return "non-permissive"
157 |
158 | # --- manage_stub function ---
159 | # (This function's core logic for creating/updating/deleting files remains unchanged.
160 | # It will use the updated STUB_WORKFLOW_CONTENT_TEMPLATE.)
161 | def manage_stub(repo_full_name, gh_token):
162 | g = Github(gh_token)
163 | try:
164 | repo = g.get_repo(repo_full_name)
165 | if repo.archived: logging.info(f"Skipping archived repository: {repo_full_name}"); return "skipped_archived"
166 | except UnknownObjectException: logging.warning(f"Repository {repo_full_name} not found or PAT lacks access during stub management."); return "error_repo_not_found_stub_mgmt"
167 | except Exception as e: logging.error(f"Error accessing repository {repo_full_name} object for stub management: {e}"); return "error_repo_access_stub_mgmt"
168 |
169 | logging.info(f"Managing stub for repository: {repo_full_name}")
170 | spdx_id, license_type = get_license_info(repo_full_name, gh_token)
171 | logging.info(f" License classification for {repo_full_name}: {license_type} (SPDX/Code: {spdx_id or 'N/A'})")
172 |
173 | action_taken = "no_action_default"
174 | if license_type == "non-permissive":
175 | logging.info(f" Non-permissive license. Ensuring CLA stub workflow exists for {repo_full_name}.")
176 | try:
177 | existing_stub_file = None; existing_content = ""
178 | try:
179 | existing_stub_file = repo.get_contents(STUB_WORKFLOW_PATH, ref=repo.default_branch)
180 | existing_content = base64.b64decode(existing_stub_file.content).decode('utf-8')
181 | except UnknownObjectException: logging.info(f" No existing stub found at {STUB_WORKFLOW_PATH} in {repo_full_name}.")
182 | current_version_str = "0.0.0"
183 | if existing_content:
184 | for line in existing_content.splitlines():
185 | if "# Version:" in line: current_version_str = line.split("# Version:")[1].strip(); break
186 | if existing_stub_file and current_version_str == TARGET_STUB_VERSION and existing_content.strip() == STUB_WORKFLOW_CONTENT_TEMPLATE.strip():
187 | logging.info(f" CLA stub '{STUB_WORKFLOW_PATH}' is up-to-date (v{TARGET_STUB_VERSION}) in {repo_full_name}.")
188 | action_taken = "skipped_stub_up_to_date"
189 | elif existing_stub_file:
190 | commit_message = f"ci: Update CLA trigger workflow to v{TARGET_STUB_VERSION}"
191 | logging.info(f" Updating existing CLA stub '{STUB_WORKFLOW_PATH}' (Old: v{current_version_str}) in {repo_full_name}.")
192 | repo.update_file(STUB_WORKFLOW_PATH, commit_message, STUB_WORKFLOW_CONTENT_TEMPLATE, existing_stub_file.sha, branch=repo.default_branch)
193 | action_taken = "stub_updated"
194 | else:
195 | commit_message = f"ci: Add CLA trigger workflow v{TARGET_STUB_VERSION}"
196 | logging.info(f" CLA stub '{STUB_WORKFLOW_PATH}' not found. Creating in {repo_full_name}.")
197 | repo.create_file(STUB_WORKFLOW_PATH, commit_message, STUB_WORKFLOW_CONTENT_TEMPLATE, branch=repo.default_branch)
198 | action_taken = "stub_created"
199 | except GithubException as e: logging.error(f" GitHub API error (stub for non-permissive {repo_full_name}): Status {e.status}, Data {e.data}"); action_taken = f"error_api_non_permissive_{e.status}"
200 | except Exception as e: logging.error(f" Unexpected error (stub for non-permissive {repo_full_name}): {e}"); action_taken = "error_unknown_non_permissive"
201 | elif license_type == "permissive":
202 | logging.info(f" Permissive license ({spdx_id}). Ensuring CLA stub does NOT exist for {repo_full_name}.")
203 | try:
204 | existing_stub_file = repo.get_contents(STUB_WORKFLOW_PATH, ref=repo.default_branch)
205 | commit_message = f"ci: Remove CLA trigger workflow (license: {spdx_id} is permissive)"
206 | logging.info(f" Permissive license; removing existing CLA stub '{STUB_WORKFLOW_PATH}' from {repo_full_name}.")
207 | repo.delete_file(STUB_WORKFLOW_PATH, commit_message, existing_stub_file.sha, branch=repo.default_branch)
208 | action_taken = "stub_removed_permissive"
209 | except UnknownObjectException: logging.info(f" Permissive license; CLA stub '{STUB_WORKFLOW_PATH}' does not exist. No action needed."); action_taken = "skipped_permissive_no_stub"
210 | except GithubException as e: logging.error(f" GitHub API error (removing stub for permissive {repo_full_name}): Status {e.status}, Data {e.data}"); action_taken = f"error_api_permissive_{e.status}"
211 | except Exception as e: logging.error(f" Unexpected error (removing stub for permissive {repo_full_name}): {e}"); action_taken = "error_unknown_permissive"
212 | else: logging.warning(f" Unknown license type '{license_type}' for {repo_full_name} (SPDX/Code: {spdx_id}). No action on stub."); action_taken = "skipped_unknown_license_type"
213 | return action_taken
214 |
215 | # --- __main__ function ---
216 | # (This function remains unchanged)
217 | if __name__ == "__main__":
218 | repo_to_process = os.environ.get("TARGET_REPO_FULL_NAME")
219 | org_pat = os.environ.get("ORG_PAT")
220 | if not repo_to_process: logging.critical("CRITICAL: TARGET_REPO_FULL_NAME not set."); exit(1)
221 | if not org_pat: logging.critical("CRITICAL: ORG_PAT not set."); exit(1)
222 | max_retries = 1; final_status = "error_unknown_initial"
223 | for attempt in range(max_retries + 1):
224 | try:
225 | final_status = manage_stub(repo_to_process, org_pat)
226 | if not final_status.startswith("error_"): break
227 | except Exception as e:
228 | logging.error(f"Attempt {attempt+1} for {repo_to_process} failed with unhandled exception: {e}")
229 | final_status = f"error_unhandled_exception_attempt_{attempt+1}"
230 | if attempt < max_retries and final_status.startswith("error_"):
231 | sleep_duration = (attempt + 1) * 10
232 | logging.info(f"Retrying {repo_to_process} in {sleep_duration}s after status: {final_status}"); time.sleep(sleep_duration)
233 | elif attempt == max_retries:
234 | logging.error(f"All {max_retries+1} attempts failed for {repo_to_process}. Final status: {final_status}")
235 | print(f"REPO_PROCESSED_NAME={repo_to_process}")
236 | print(f"REPO_PROCESSED_STATUS={final_status}")
237 | if final_status.startswith("error_"): exit(1)
238 |
239 |
240 |
--------------------------------------------------------------------------------
/.github/scripts/scan_org_licenses_licensee.py:
--------------------------------------------------------------------------------
1 | import os
2 | import subprocess
3 | import json
4 | import tempfile
5 | import shutil
6 | import time
7 | import sys
8 | from github import Github, GithubException, RateLimitExceededException, UnknownObjectException
9 |
10 | MAX_RETRIES_CLONE = 3
11 | RETRY_DELAY_SECONDS = 10
12 | LICENSEE_CONFIDENCE_THRESHOLD = "70" # Lowered confidence threshold
13 |
14 | def run_command_robust(command_args, cwd=None, check_return_code=True, an_input=None):
15 | """
16 | Runs a shell command, captures its output, and handles errors robustly.
17 | Returns a tuple: (success, stdout, stderr)
18 | """
19 | try:
20 | process = subprocess.Popen(
21 | command_args,
22 | cwd=cwd,
23 | stdin=subprocess.PIPE if an_input else None,
24 | stdout=subprocess.PIPE,
25 | stderr=subprocess.PIPE,
26 | text=True,
27 | env=os.environ.copy()
28 | )
29 | stdout, stderr = process.communicate(input=an_input)
30 |
31 | if check_return_code and process.returncode != 0:
32 | print(f"Command failed with exit code {process.returncode}: {' '.join(command_args)}")
33 | print(f"Stderr: {stderr.strip()}")
34 | return False, stdout.strip(), stderr.strip()
35 | return True, stdout.strip(), stderr.strip()
36 |
37 | except FileNotFoundError:
38 | print(f"Error: Command not found - {command_args[0]}. Ensure it's installed and in PATH.")
39 | return False, "", f"Command not found: {command_args[0]}"
40 | except Exception as e:
41 | print(f"An unexpected error occurred while running command {' '.join(command_args)}: {e}")
42 | return False, "", str(e)
43 |
44 | def extract_license_from_entry(license_entry_obj):
45 | """Helper to extract SPDX ID or name from a license object/dictionary."""
46 | if not isinstance(license_entry_obj, dict):
47 | return None
48 |
49 | spdx_id = license_entry_obj.get("spdx_id")
50 | if spdx_id and spdx_id != "NOASSERTION":
51 | return spdx_id
52 |
53 | name = license_entry_obj.get("name")
54 | if name:
55 | return name
56 | return None
57 |
58 |
59 | def detect_license_with_licensee_cli(repo_dir_path):
60 | """Runs licensee detect in the given directory and parses the output."""
61 | command = ["licensee", "detect", "--json", ".", f"--confidence={LICENSEE_CONFIDENCE_THRESHOLD}"]
62 | success, stdout_raw, stderr_raw = run_command_robust(command, cwd=repo_dir_path, check_return_code=False)
63 |
64 | if not stdout_raw and not success:
65 | print(f"Licensee CLI failed to execute in {repo_dir_path}. Stderr: {stderr_raw}")
66 | return "LICENSEE_EXECUTION_ERROR"
67 |
68 | json_output_str = stdout_raw.strip()
69 | if not json_output_str or json_output_str == "null":
70 | if "No license found" in stderr_raw.lower():
71 | return "NONE_FOUND_BY_LICENSEE"
72 | print(f"Licensee produced empty or null JSON output for {repo_dir_path} with confidence {LICENSEE_CONFIDENCE_THRESHOLD}. Stderr: {stderr_raw}")
73 | return "LICENSEE_EMPTY_OUTPUT"
74 |
75 | try:
76 | license_data = json.loads(json_output_str)
77 | if not license_data:
78 | return "NONE_FOUND_BY_LICENSEE" # Empty JSON object means no license
79 |
80 | # print(f"DEBUG: Full licensee JSON for {repo_dir_path} (Confidence: {LICENSEE_CONFIDENCE_THRESHOLD}): {json_output_str}")
81 |
82 | # Attempt 1: "matched_license" - This is usually licensee's primary determination
83 | matched_license_obj = license_data.get("matched_license")
84 | license_id_from_matched = extract_license_from_entry(matched_license_obj)
85 | if license_id_from_matched:
86 | # print(f"Found via 'matched_license': {license_id_from_matched} for {repo_dir_path}")
87 | return license_id_from_matched
88 |
89 | # Attempt 2: "licenses" array - If matched_license is null or not conclusive
90 | licenses_array = license_data.get("licenses")
91 | if isinstance(licenses_array, list) and licenses_array:
92 | # print(f"DEBUG: 'matched_license' was not conclusive for {repo_dir_path}. Examining 'licenses' array (length {len(licenses_array)}).")
93 |
94 | def get_confidence(lic_entry_dict): # Renamed for clarity
95 | conf = lic_entry_dict.get("confidence")
96 | try: return float(conf) if conf is not None else 0.0
97 | except (ValueError, TypeError): return 0.0
98 |
99 | # Filter out entries that are not dictionaries before sorting
100 | valid_license_entries = [entry for entry in licenses_array if isinstance(entry, dict)]
101 |
102 | if not valid_license_entries:
103 | # print(f"DEBUG: 'licenses' array for {repo_dir_path} contained no valid dictionary entries.")
104 | # Fall through to check matched_files or return NONE_FOUND
105 | pass
106 | else:
107 | sorted_licenses = sorted(
108 | valid_license_entries,
109 | key=lambda lic_entry_dict: (lic_entry_dict.get("featured") is True, get_confidence(lic_entry_dict)),
110 | reverse=True
111 | )
112 |
113 | if sorted_licenses: # Should always be true if valid_license_entries was not empty
114 | best_license_entry = sorted_licenses[0]
115 | license_id_from_array = extract_license_from_entry(best_license_entry)
116 | if license_id_from_array:
117 | # print(f"Found via 'licenses' array (best after sort): {license_id_from_array} for {repo_dir_path}")
118 | return license_id_from_array
119 |
120 | # Attempt 3: Check "matched_files" array as a fallback if the above didn't yield anything.
121 | # This is less ideal for a single repository-wide license but can be an indicator.
122 | # We'll take the first usable license found in any matched file.
123 | matched_files_array = license_data.get("matched_files")
124 | if isinstance(matched_files_array, list) and matched_files_array:
125 | # print(f"DEBUG: No clear license from 'matched_license' or 'licenses'. Checking 'matched_files' for {repo_dir_path}.")
126 | for file_match_entry in matched_files_array:
127 | if isinstance(file_match_entry, dict):
128 | license_in_file_obj = file_match_entry.get("license") # The 'license' object within a matched_file
129 | license_id_from_file = extract_license_from_entry(license_in_file_obj)
130 | if license_id_from_file:
131 | # print(f"Found via 'matched_files[x].license': {license_id_from_file} from file {file_match_entry.get('filename')} for {repo_dir_path}")
132 | return license_id_from_file
133 | # print(f"DEBUG: 'matched_files' array was present for {repo_dir_path} but no usable ID found within its entries' 'license' objects.")
134 |
135 |
136 | # If none of the above parsing strategies yielded a result
137 | print(f"DEBUG: No conclusive license found after all parsing strategies for {repo_dir_path} (Confidence: {LICENSEE_CONFIDENCE_THRESHOLD}).")
138 | print(f"DEBUG: Full licensee JSON for {repo_dir_path}: {json_output_str}") # This log is CRITICAL
139 | return "NONE_FOUND_BY_LICENSEE"
140 |
141 | except json.JSONDecodeError:
142 | print(f"Error decoding JSON from licensee output for {repo_dir_path}: {json_output_str}")
143 | return "LICENSEE_JSON_ERROR"
144 | except Exception as e:
145 | print(f"Unexpected error parsing licensee output for {repo_dir_path}: {e}. JSON was: {json_output_str}")
146 | return "LICENSEE_PARSE_ERROR"
147 |
148 |
149 |
150 | def main():
151 | organization_name = os.environ.get("ORGANIZATION_TO_SCAN")
152 | github_token = os.environ.get("GH_TOKEN_FOR_SCAN")
153 | output_filename = os.environ.get("OUTPUT_FILENAME_TO_USE", "organization_public_licenses_licensee.json")
154 |
155 | if not organization_name:
156 | print("Error: ORGANIZATION_TO_SCAN environment variable is not set.")
157 | sys.exit(1)
158 | if not github_token:
159 | print("Error: GH_TOKEN_FOR_SCAN environment variable is not set. Token is needed for API and git operations.")
160 | sys.exit(1)
161 |
162 | print(f"Python script starting scan for organization: {organization_name} using licensee CLI with confidence >= {LICENSEE_CONFIDENCE_THRESHOLD}%")
163 | print(f"Output file will be: {output_filename}")
164 |
165 | g = None
166 | try:
167 | g = Github(github_token)
168 | try:
169 | user = g.get_user()
170 | print(f"Authenticated to GitHub API as: {user.login}")
171 | rate_limit_info = g.get_rate_limit().core
172 | reset_time_str = rate_limit_info.reset.strftime('%Y-%m-%d %H:%M:%S UTC') if rate_limit_info.reset else 'N/A'
173 | print(f"API Rate limit: {rate_limit_info.remaining}/{rate_limit_info.limit}, Resets at: {reset_time_str}")
174 | except GithubException as ge_user:
175 | is_integration_error = ge_user.status == 403 and "integration" in str(ge_user.data).lower()
176 | is_forbidden_generic = ge_user.status == 403
177 |
178 | if is_integration_error:
179 | print(f"Warning (non-critical): Could not get authenticated user info (g.get_user()): {ge_user.status} - {ge_user.data}. This can happen with GITHUB_TOKEN. Proceeding...")
180 | elif is_forbidden_generic:
181 | print(f"Warning (potentially critical): GET /user failed with 403 Forbidden: {ge_user.data}. The provided token may lack 'read:user' or similar scope if it's a PAT. Proceeding cautiously...")
182 | else:
183 | print(f"Error during g.get_user() call: {ge_user.status} - {ge_user.data}")
184 | if ge_user.status == 401:
185 | print("This is a 401 Unauthorized error. The token is likely invalid or expired. Exiting.")
186 | sys.exit(1)
187 | print("Proceeding, but initial user verification failed with an unexpected error.")
188 |
189 | if g:
190 | try:
191 | rate_limit_info = g.get_rate_limit().core
192 | reset_time_str = rate_limit_info.reset.strftime('%Y-%m-%d %H:%M:%S UTC') if rate_limit_info.reset else 'N/A'
193 | print(f"API Rate limit (fetched separately): {rate_limit_info.remaining}/{rate_limit_info.limit}, Resets at: {reset_time_str}")
194 | except Exception as e_rl:
195 | print(f"Warning: Could not fetch rate limit information separately: {e_rl}")
196 | except Exception as e_user_other:
197 | print(f"Unexpected error during g.get_user() or initial rate limit check: {e_user_other}")
198 | print("Proceeding despite this initial error.")
199 |
200 | except Exception as e_init:
201 | print(f"CRITICAL Error initializing PyGithub object with token: {e_init}. This usually means the token is malformed or there's a fundamental issue with PyGithub or network.")
202 | sys.exit(1)
203 |
204 | if not g:
205 | print("CRITICAL: PyGithub object (g) could not be initialized. Exiting.")
206 | sys.exit(1)
207 |
208 | all_licenses_info = []
209 | repo_count = 0
210 | processed_repo_count = 0
211 |
212 | try:
213 | org = g.get_organization(organization_name)
214 | print(f"Successfully fetched organization object for: {org.login}")
215 |
216 | repos_paginator = org.get_repos(type="public")
217 | print("Starting to iterate through public repositories...")
218 |
219 | for repo in repos_paginator:
220 | repo_count += 1
221 | print("-----------------------------------------------------")
222 | print(f"Processing repository: {repo.full_name} (Discovered: {repo_count})")
223 |
224 | if repo.archived:
225 | print(f"Skipping archived repository: {repo.full_name}")
226 | all_licenses_info.append({"repository_name": repo.name, "license": "ARCHIVED_REPO_SKIPPED"})
227 | continue
228 |
229 | if repo.size == 0:
230 | print(f"Skipping potentially empty repository (size 0 KB): {repo.full_name}")
231 | all_licenses_info.append({"repository_name": repo.name, "license": "EMPTY_REPO_SKIPPED"})
232 | continue
233 |
234 | current_license_info = {"repository_name": repo.name, "license": "ERROR_PROCESSING_REPO"}
235 | temp_clone_dir = tempfile.mkdtemp(prefix=f"repo_licensee_{repo.name.replace('/', '_')}_")
236 |
237 | cloned_successfully = False
238 | for attempt in range(1, MAX_RETRIES_CLONE + 1):
239 | clone_command = ["git", "clone", "--depth", "1", "--quiet", repo.clone_url, temp_clone_dir]
240 |
241 | if attempt > 1:
242 | for item_name in os.listdir(temp_clone_dir):
243 | item_path = os.path.join(temp_clone_dir, item_name)
244 | try:
245 | if os.path.isdir(item_path) and not os.path.islink(item_path):
246 | shutil.rmtree(item_path)
247 | else:
248 | os.unlink(item_path)
249 | except Exception as e_rm:
250 | print(f"Warning: Failed to remove {item_path} for retry: {e_rm}")
251 |
252 | success, stdout_clone, stderr_clone = run_command_robust(clone_command, check_return_code=True)
253 |
254 | if success:
255 | cloned_successfully = True
256 | break
257 | else:
258 | print(f"Clone failed for {repo.full_name} (attempt {attempt}). Stderr: {stderr_clone}")
259 | if attempt < MAX_RETRIES_CLONE:
260 | time.sleep(RETRY_DELAY_SECONDS)
261 | else:
262 | print(f"Max retries reached for cloning {repo.full_name}.")
263 | current_license_info["license"] = "ERROR_CLONING"
264 |
265 | if cloned_successfully:
266 | license_id = detect_license_with_licensee_cli(temp_clone_dir)
267 | current_license_info["license"] = license_id
268 | print(f"License for {repo.name}: {license_id}")
269 |
270 | all_licenses_info.append(current_license_info)
271 | processed_repo_count +=1
272 |
273 | try:
274 | shutil.rmtree(temp_clone_dir)
275 | except Exception as e_clean:
276 | print(f"Error cleaning up temp directory {temp_clone_dir}: {e_clean}")
277 |
278 | except UnknownObjectException:
279 | print(f"Error: Organization '{organization_name}' not found or not accessible via API.")
280 | except RateLimitExceededException as rle:
281 | print(f"Error: GitHub API rate limit exceeded during repository processing. {rle.data}")
282 | except GithubException as ge:
283 | print(f"GitHub API error during repository processing: {ge.status} - {ge.data}")
284 | except Exception as e:
285 | print(f"An unexpected error occurred during main processing loop: {e}")
286 | finally:
287 | with open(output_filename, "w") as f_out:
288 | json.dump(all_licenses_info, f_out, indent=2)
289 | print(f"Output file '{output_filename}' written with {len(all_licenses_info)} entries (discovered {repo_count} repos, attempted to process {processed_repo_count}).")
290 |
291 | print("-----------------------------------------------------")
292 | print(f"Python + Licensee CLI: Public license scan finished. Report: {output_filename}")
293 | if repo_count == 0:
294 | print("No public repositories were discovered for this organization.")
295 | elif processed_repo_count == 0 and repo_count > 0:
296 | print(f"Discovered {repo_count} repositories, but none were processed (e.g., all archived/empty or errors before processing).")
297 |
298 | if __name__ == "__main__":
299 | main()
300 |
301 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-scan.yml:
--------------------------------------------------------------------------------
1 | name: Dependency Scan
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * 1' # Run weekly on Monday at midnight UTC
6 | workflow_dispatch:
7 | inputs:
8 | org_name:
9 | description: 'GitHub Organization Name (optional)'
10 | required: false
11 | repo_list:
12 | description: 'Comma-separated list of repositories (optional)'
13 | required: false
14 | log_level:
15 | description: 'Logging level'
16 | required: false
17 | type: choice
18 | options:
19 | - INFO
20 | - DEBUG
21 | - WARNING
22 | - ERROR
23 | default: 'INFO'
24 | vulnerability_threshold:
25 | description: 'Number of vulnerabilities to trigger issue creation'
26 | required: false
27 | type: number
28 | default: 10
29 |
30 | permissions:
31 | security-events: read
32 | contents: write # Needed for committing the report
33 | issues: write # Needed for creating issues
34 |
35 | jobs:
36 | scan:
37 | runs-on: ubuntu-latest
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v4
42 | with:
43 | repository: ${{ github.repository_owner }}/.github # Checkout .github repo
44 | ref: main # Or your default branch
45 | token: ${{ secrets.DEPENDENCY_SCAN_TOKEN }} # Use a PAT or GitHub App token with appropriate permissions!
46 |
47 | - name: Set up Python
48 | uses: actions/setup-python@v5
49 | with:
50 | python-version: '3.10'
51 | cache: 'pip'
52 | cache-dependency-path: scripts/requirements.txt
53 |
54 | - name: Install dependencies
55 | run: |
56 | python -m pip install --upgrade pip
57 | pip install requests
58 |
59 | - name: Generate timestamp
60 | id: timestamp
61 | run: echo "timestamp=$(date +%Y%m%d_%H%M%S)" >> $GITHUB_OUTPUT
62 |
63 | - name: Run dependency scan
64 | id: run-scan
65 | env:
66 | GITHUB_TOKEN: ${{ secrets.DEPENDENCY_SCAN_TOKEN }}
67 | ORG_NAME: ${{ github.event.inputs.org_name || github.repository_owner }}
68 | REPO_LIST: ${{ github.event.inputs.repo_list }}
69 | REPORT_FILE: "vulnerability_report_${{ steps.timestamp.outputs.timestamp }}.csv"
70 | run: |
71 |
72 | COMMAND="python scripts/dependency_scanner.py \
73 | --token $GITHUB_TOKEN \
74 | --output $REPORT_FILE \
75 | --log-level ${{ github.event.inputs.log_level || 'INFO' }} \
76 | --max-workers ${{ github.event.inputs.max_workers || 10 }} \
77 | --max-retries 3"
78 |
79 | if [[ -n "$ORG_NAME" ]]; then
80 | COMMAND="$COMMAND --org $ORG_NAME"
81 | fi
82 |
83 | if [[ -n "$REPO_LIST" ]]; then
84 | COMMAND="$COMMAND --repo-list $REPO_LIST"
85 | fi
86 |
87 | $COMMAND
88 | echo "report_path=reports/$REPORT_FILE" >> $GITHUB_OUTPUT
89 |
90 |
91 | - name: Check for No Repositories
92 | id: check-repos
93 | if: success()
94 | run: |
95 | if grep -q "__NO_REPOS__" ${{ steps.run-scan.outputs.report_path }}/../output.txt; then
96 | echo "No repositories found in the organization. Exiting."
97 | exit 1
98 | fi
99 |
100 | - name: Process report statistics (inline)
101 | id: stats
102 | if: success() && steps.check-repos.outcome == 'success'
103 | run: |
104 | STATS=$(grep "__STATS_START__" ${{ steps.run-scan.outputs.report_path }}/../output.txt | sed 's/__STATS_START__//' | sed 's/__STATS_END__//')
105 | echo "total_vulnerabilities=$(echo $STATS | cut -d',' -f1 | cut -d'=' -f2)" >> $GITHUB_OUTPUT
106 | echo "processed_repos=$(echo $STATS | cut -d',' -f2 | cut -d'=' -f2)" >> $GITHUB_OUTPUT
107 | echo "Total vulnerabilities found: $(echo $STATS | cut -d',' -f1 | cut -d'=' -f2)"
108 | echo "Processed repos: $(echo $STATS | cut -d',' -f2 | cut -d'=' -f2)"
109 |
110 |
111 | - name: Create summary issue (using github-script)
112 | if: success() && steps.check-repos.outcome == 'success' && steps.stats.outputs.total_vulnerabilities > inputs.vulnerability_threshold
113 | uses: actions/github-script@v7
114 | with:
115 | script: |
116 | const stats = {
117 | total: '${{ steps.stats.outputs.total_vulnerabilities }}',
118 | processedRepos: '${{ steps.stats.outputs.processed_repos }}',
119 | };
120 |
121 | const now = new Date();
122 | const formattedDate = now.toLocaleDateString('en-US', {
123 | year: 'numeric',
124 | month: 'long',
125 | day: 'numeric'
126 | });
127 |
128 | const body = `
129 | # Dependency Vulnerability Report Summary
130 |
131 | Report generated on: ${now.toISOString()}
132 |
133 | ## Statistics
134 | - Total vulnerabilities found: ${stats.total}
135 | - Repositories processed: ${stats.processedRepos}
136 |
137 | ## Details
138 | - Report artifact: [Download report](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
139 | - Workflow run: [View details](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
140 |
141 | ## Configuration
142 | - Log level: ${{ github.event.inputs.log_level || 'INFO' }}
143 | - Vulnerability threshold: ${{ github.event.inputs.vulnerability_threshold || '10'}}
144 | `;
145 |
146 | await github.rest.issues.create({
147 | owner: context.repo.owner,
148 | repo: context.repo.repo,
149 | title: \`⚠️ Dependency Vulnerability Report - \${formattedDate}\`,
150 | body: body,
151 | labels: ['dependency-vulnerability', 'report']
152 | });
153 |
154 | - name: Commit and Push Report
155 | if: success() && steps.check-repos.outcome == 'success'
156 | uses: stefanzweifel/git-auto-commit-action@v5
157 | with:
158 | commit_message: "Add dependency vulnerability report: ${{ steps.timestamp.outputs.timestamp }}"
159 | repository: ./ # Commit to the root of the checked-out repo
160 | file_pattern: reports/*.csv
161 | commit_user_name: GitHub Actions
162 | commit_user_email: actions@github.com
163 | commit_author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
164 | push_options: '--force'
165 | token: ${{ secrets.DEPENDENCY_SCAN_TOKEN }}
166 |
167 | - name: Notify on failure
168 | if: failure()
169 | uses: actions/github-script@v7
170 | with:
171 | script: |
172 | const body = `
173 | # 🚨 Dependency Vulnerability Report Generation Failed
174 |
175 | Workflow run failed at ${new Date().toISOString()}
176 |
177 | ## Details
178 | - Run ID: \`${context.runId}\`
179 | - Trigger: ${context.eventName}
180 | - Actor: @${context.actor}
181 |
182 | ## Links
183 | - [View run details](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
184 | - [View workflow file](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/blob/main/.github/workflows/dependency-scan.yml)
185 |
186 | Please check the workflow logs for detailed error information.
187 | `;
188 |
189 | await github.rest.issues.create({
190 | owner: context.repo.owner,
191 | repo: context.repo.repo,
192 | title: '🚨 Dependency Vulnerability Report Generation Failed',
193 | body: body,
194 | labels: ['dependency-vulnerability', 'failed']
195 | });
196 |
197 | - name: Clean up
198 | if: always()
199 | run: |
200 | echo "No clean up required."
201 |
202 | concurrency:
203 | group: ${{ github.workflow }}-${{ github.ref }}
204 | cancel-in-progress: true
205 |
206 |
--------------------------------------------------------------------------------
/.github/workflows/license-cla-check.yml:
--------------------------------------------------------------------------------
1 | name: License Compliance & CLA Gate
2 |
3 | on:
4 | pull_request_target:
5 | types: [opened, reopened, synchronize]
6 | issue_comment:
7 | types: [created]
8 |
9 | permissions:
10 | contents: read
11 | pull-requests: write
12 |
13 | jobs:
14 | license-check:
15 | # Run on PR events or CLA-related comments
16 | if: |
17 | github.event_name == 'pull_request_target' ||
18 | (github.event_name == 'issue_comment' &&
19 | github.event.issue.pull_request &&
20 | (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA'))
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Debug Event Payload
24 | run: |
25 | echo "Event Name: ${{ github.event_name }}"
26 | echo "Repository: ${{ github.repository }}"
27 | echo "Enabled Repos: ${{ vars.ENABLED_REPOS }}"
28 | echo "Excluded Repos: ${{ vars.EXCLUDED_REPOS }}"
29 |
30 | - name: Check if repository is enabled or excluded
31 | id: repo_filter
32 | shell: python
33 | env:
34 | ENABLED_REPOS: ${{ vars.ENABLED_REPOS || '[]' }}
35 | EXCLUDED_REPOS: ${{ vars.EXCLUDED_REPOS || '[]' }}
36 | CURRENT_REPO: ${{ github.repository }}
37 | run: |
38 | import os
39 | import json
40 |
41 | try:
42 | # Load repository lists from environment variables
43 | enabled_repos = json.loads(os.environ['ENABLED_REPOS'])
44 | excluded_repos = json.loads(os.environ['EXCLUDED_REPOS'])
45 | current_repo = os.environ['CURRENT_REPO']
46 |
47 | # Check enabled repos first if specified
48 | if enabled_repos:
49 | if current_repo not in enabled_repos:
50 | print(f"⚠️ Skipping {current_repo} - not in enabled list")
51 | print(f"Enabled repos: {', '.join(enabled_repos)}")
52 | with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
53 | fh.write('should_run=false')
54 | exit(0)
55 |
56 | # Then check excluded repos
57 | if current_repo in excluded_repos:
58 | print(f"⚠️ Skipping {current_repo} - in excluded list")
59 | print(f"Excluded repos: {', '.join(excluded_repos)}")
60 | with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
61 | fh.write('should_run=false')
62 | exit(0)
63 |
64 | # Repository is allowed to run
65 | print(f"✅ Running checks for {current_repo}")
66 | with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
67 | fh.write('should_run=true')
68 |
69 | except json.JSONDecodeError as e:
70 | print(f"❌ Error parsing repository lists: {e}")
71 | print(f"ENABLED_REPOS: {os.environ['ENABLED_REPOS']}")
72 | print(f"EXCLUDED_REPOS: {os.environ['EXCLUDED_REPOS']}")
73 | with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
74 | fh.write('should_run=false')
75 | exit(1)
76 |
77 | - name: Get PR SHA
78 | id: pr_sha
79 | if: steps.repo_filter.outputs.should_run == 'true'
80 | shell: python
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83 | EVENT_NAME: ${{ github.event_name }}
84 | EVENT_PATH: ${{ github.event_path }}
85 | REPOSITORY: ${{ github.repository }}
86 | run: |
87 | import os
88 | import json
89 | from github import Github
90 |
91 | try:
92 | event_name = os.environ['EVENT_NAME']
93 |
94 | # For pull_request_target events, get SHA directly from event payload
95 | if event_name == 'pull_request_target':
96 | with open(os.environ['EVENT_PATH']) as f:
97 | event_data = json.load(f)
98 | sha = event_data['pull_request']['head']['sha']
99 | print(f"✅ Got SHA from pull_request_target event: {sha}")
100 |
101 | # For issue_comment events, need to fetch PR details
102 | else:
103 | g = Github(os.environ['GITHUB_TOKEN'])
104 | repo = g.get_repo(os.environ['REPOSITORY'])
105 | with open(os.environ['EVENT_PATH']) as f:
106 | event_data = json.load(f)
107 | pr_number = event_data['issue']['number']
108 |
109 | pr = repo.get_pull(pr_number)
110 | sha = pr.head.sha
111 | print(f"✅ Got SHA from PR #{pr_number}: {sha}")
112 |
113 | # Write SHA to output
114 | with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
115 | fh.write(f'sha={sha}')
116 |
117 | except Exception as e:
118 | print(f"❌ Error getting PR SHA: {e}")
119 | exit(1)
120 |
121 | - name: Checkout PR Head
122 | if: steps.repo_filter.outputs.should_run == 'true'
123 | uses: actions/checkout@v4
124 | with:
125 | ref: ${{ steps.pr_sha.outputs.sha }}
126 | fetch-depth: 0
127 |
128 | - name: Fuzzy License Validation
129 | if: steps.repo_filter.outputs.should_run == 'true'
130 | id: license_check
131 | uses: ./.github/actions/check-license
132 |
133 | - name: Enforce CLA
134 | if: steps.license_check.outputs.license_status == 'non-permissive' && steps.repo_filter.outputs.should_run == 'true'
135 | uses: contributor-assistant/github-action@v2.6.1
136 | env:
137 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
138 | with:
139 | path-to-signatures: 'signatures/version1/${{ github.repository }}/cla.json'
140 | path-to-document: 'https://${{ github.repository_owner }}.github.io/cla-docs/GenericCLA.html'
141 | branch: 'main'
142 | allowlist: 'org-admin,bot*'
143 | # Optional inputs (uncomment and customize as needed)
144 | # remote-organization-name: 'your-remote-org'
145 | # remote-repository-name: 'your-remote-repo'
146 | # create-file-commit-message: 'Creating file for storing CLA Signatures'
147 | # signed-commit-message: '$contributorName has signed the CLA in $owner/$repo#$pullRequestNo'
148 | # custom-notsigned-prcomment: 'Please sign the CLA to contribute.'
149 | # custom-pr-sign-comment: 'I have read the CLA Document and I hereby sign the CLA'
150 | # custom-allsigned-prcomment: 'All contributors have signed the CLA.'
151 | # lock-pullrequest-aftermerge: false
152 | # use-dco-flag: false
153 |
--------------------------------------------------------------------------------
/.github/workflows/manage-cla-stubs.yml:
--------------------------------------------------------------------------------
1 | # File: .github/.github/workflows/manage-cla-stubs.yml
2 | # Purpose: Periodically manages CLA trigger stubs across organization repositories.
3 | # Builds the licensee Docker image within each processing job.
4 |
5 | name: Manage CLA Stubs Org-Wide
6 |
7 | on:
8 | # schedule:
9 | # - cron: '0 3 * * *'
10 | workflow_dispatch:
11 | inputs:
12 | specific_repos:
13 | description: 'Comma-separated list of specific repos (owner/repo1,owner/repo2) to process. Overrides INCLUDE/EXCLUDE.'
14 | required: false
15 | default: ''
16 | licensee_version_tag:
17 | description: 'Licensee git tag to build (e.g., v9.16.1). Default is v9.16.1.'
18 | required: false
19 | default: 'v9.16.1' # Pin to a known stable version of licensee
20 |
21 | permissions: {}
22 |
23 | env:
24 | EXCLUDE_REPOS_CSV: ${{ vars.EXCLUDE_REPOS || '.github' }}
25 | INCLUDE_REPOS_CSV: ${{ vars.INCLUDE_REPOS || '' }}
26 | PERMISSIVE_SPDX_IDS_CSV: ${{ vars.PERMISSIVE_SPDX_IDS || 'MIT,Apache-2.0,BSD-3-Clause,ISC,BSD-2-Clause,CC0-1.0,Unlicense' }}
27 | # Define the image tag to be used consistently
28 | LOCAL_LICENSEE_IMAGE_TAG: 'local-org-licensee:latest' # This tag will be built on each runner
29 |
30 | jobs:
31 | discover_repositories:
32 | name: Discover Repositories
33 | runs-on: ubuntu-latest
34 | outputs:
35 | repositories_json: ${{ steps.get_repos.outputs.repo_list_json }}
36 | repository_count: ${{ steps.get_repos.outputs.repo_count }}
37 | steps:
38 | - name: Get Organization Repositories List
39 | id: get_repos
40 | uses: actions/github-script@v7.0.1
41 | env:
42 | INCLUDE_REPOS_CSV: ${{ env.INCLUDE_REPOS_CSV }}
43 | EXCLUDE_REPOS_CSV: ${{ env.EXCLUDE_REPOS_CSV }}
44 | SPECIFIC_REPOS_INPUT: ${{ github.event.inputs.specific_repos }}
45 | with:
46 | github-token: ${{ secrets.ORG_PAT }}
47 | script: |
48 | // ... (JavaScript for discovery - same as before) ...
49 | const includeReposList = (process.env.INCLUDE_REPOS_CSV || "").split(',').map(r => r.trim()).filter(r => r);
50 | const excludeReposList = (process.env.EXCLUDE_REPOS_CSV || "").split(',').map(r => r.trim()).filter(r => r);
51 | const specificReposInput = (process.env.SPECIFIC_REPOS_INPUT || "").split(',').map(r => r.trim()).filter(r => r);
52 | let reposToProcess = [];
53 |
54 | if (specificReposInput.length > 0) {
55 | console.log("Processing only specifically provided repositories (from manual input):", specificReposInput);
56 | reposToProcess = specificReposInput.map(repoName => {
57 | if (repoName.includes('/')) return repoName;
58 | return `${context.repo.owner}/${repoName}`;
59 | });
60 | } else if (includeReposList.length > 0) {
61 | console.log("Processing only explicitly included repositories (from VARS_INCLUDE_REPOS):", includeReposList);
62 | reposToProcess = includeReposList.map(repoName => `${context.repo.owner}/${repoName}`);
63 | } else {
64 | console.log("Fetching all non-archived repositories for organization:", context.repo.owner);
65 | for await (const response of github.paginate.iterator(github.rest.repos.listForOrg, {
66 | org: context.repo.owner, type: 'all', per_page: 100
67 | })) {
68 | for (const repo of response.data) {
69 | if (!repo.archived) { reposToProcess.push(repo.full_name); }
70 | }
71 | }
72 | console.log(`Found ${reposToProcess.length} non-archived repositories in the organization.`);
73 | }
74 |
75 | const finalRepos = reposToProcess.filter(fullName => {
76 | const repoName = fullName.split('/')[1];
77 | return !excludeReposList.includes(repoName);
78 | });
79 |
80 | console.log(`Final list of ${finalRepos.length} repositories to process:`, finalRepos);
81 | core.setOutput('repo_list_json', JSON.stringify(finalRepos));
82 | core.setOutput('repo_count', finalRepos.length);
83 |
84 | await core.summary
85 | .addHeading('Repository Discovery Phase')
86 | .addRaw(`Discovered ${finalRepos.length} repositories to process based on include/exclude rules.`)
87 | .addSeparator().write();
88 |
89 | process_repositories:
90 | name: Process Repository (${{ matrix.repository_full_name }})
91 | needs: [discover_repositories] # Does not need a separate build job anymore
92 | if: needs.discover_repositories.outputs.repository_count > 0
93 | runs-on: ubuntu-latest
94 | strategy:
95 | matrix:
96 | repository_full_name: ${{ fromJson(needs.discover_repositories.outputs.repositories_json) }}
97 | fail-fast: false
98 |
99 | steps:
100 | - name: Checkout .github repo (for our management scripts)
101 | uses: actions/checkout@v4.1.1
102 | # This checks out the content of THIS .github repository.
103 |
104 | - name: Checkout licensee source code
105 | uses: actions/checkout@v4.1.1
106 | with:
107 | repository: licensee/licensee # The official licensee repository
108 | path: licensee-src # Checkout to a subdirectory to avoid conflicts
109 | # Use the version tag from workflow_dispatch input or a default.
110 | ref: ${{ github.event.inputs.licensee_version_tag || 'v9.16.1' }}
111 | # Pin to a specific tag of licensee for stability (e.g., 'v9.16.1')
112 |
113 | - name: Build licensee Docker image locally
114 | run: |
115 | echo "Building licensee Docker image (${{ env.LOCAL_LICENSEE_IMAGE_TAG }}) from ref: ${{ github.event.inputs.licensee_version_tag || 'v9.16.1' }}"
116 | docker build ./licensee-src --tag ${{ env.LOCAL_LICENSEE_IMAGE_TAG }}
117 | echo "Successfully built Docker image: ${{ env.LOCAL_LICENSEE_IMAGE_TAG }}"
118 | # Verify image exists locally
119 | docker images ${{ env.LOCAL_LICENSEE_IMAGE_TAG }}
120 |
121 | - name: DEBUG - Test ORG_PAT Access to ${{ matrix.repository_full_name }}
122 | if: matrix.repository_full_name == 'vmware/test-cla-gpl2' # Only run for one repo for testing
123 | uses: actions/github-script@v7.0.1
124 | with:
125 | github-token: ${{ secrets.ORG_PAT }}
126 | script: |
127 | const repoFullName = '${{ matrix.repository_full_name }}';
128 | const owner = repoFullName.split('/')[0];
129 | const repo = repoFullName.split('/')[1];
130 | core.info(`Attempting to get repo details for ${repoFullName} using ORG_PAT.`);
131 | try {
132 | const { data: repoData } = await github.rest.repos.get({ owner, repo });
133 | core.info(`Successfully fetched repo data: ${JSON.stringify(repoData.name)}`);
134 |
135 | core.info(`Attempting to get root contents for ${repoFullName}`);
136 | const { data: contents } = await github.rest.repos.getContent({ owner, repo, path: '' });
137 | core.info(`Successfully fetched root contents. Number of items: ${contents.length}`);
138 |
139 | // Attempt a "safe" write-like check, e.g., trying to get a specific workflow file,
140 | // or even just trying to create a dummy branch (which requires write access).
141 | // For now, just reading content which `ORG_PAT` *should* be able to do.
142 | // The Python script is failing on a repo.create_file() call.
143 | } catch (error) {
144 | core.setFailed(`ORG_PAT test failed for ${repoFullName}: ${error.message}`);
145 | core.error(JSON.stringify(error)); // Log the full error object
146 | }
147 |
148 | - name: Set up Python
149 | uses: actions/setup-python@v5.0.0
150 | with:
151 | python-version: '3.10'
152 |
153 | - name: Install Python dependencies
154 | run: pip install PyGithub==1.59.1
155 |
156 | - name: Check license and manage CLA stub for ${{ matrix.repository_full_name }}
157 | id: manage_stub_step
158 | run: python .github/scripts/check_and_install_stub.py
159 | env:
160 | TARGET_REPO_FULL_NAME: ${{ matrix.repository_full_name }}
161 | ORG_PAT: ${{ secrets.ORG_PAT }}
162 | PERMISSIVE_SPDX_IDS: ${{ env.PERMISSIVE_SPDX_IDS_CSV }}
163 | GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }}
164 | # Pass the locally built image tag to the Python script.
165 | LICENSEE_DOCKER_IMAGE: ${{ env.LOCAL_LICENSEE_IMAGE_TAG }}
166 |
167 | - name: Record action for ${{ matrix.repository_full_name }} to Job Summary
168 | if: always() # Run even if previous steps fail, to capture status.
169 | run: |
170 | echo "Debug: manage_stub_step stdout was: [${{ steps.manage_stub_step.outputs.stdout }}]"
171 | echo "Debug: manage_stub_step outcome was: [${{ steps.manage_stub_step.outcome }}]"
172 |
173 | # Attempt to parse the status from stdout if available
174 | # Initialize FINAL_STATUS to reflect the outcome if stdout parsing fails
175 | FINAL_STATUS="status_unknown_due_to_step_failure"
176 | if [[ "${{ steps.manage_stub_step.outcome }}" == "success" ]]; then
177 | FINAL_STATUS="success_script_did_not_error" # Default if successful but no specific status
178 | fi
179 |
180 | # Try to get the more specific status if stdout is available
181 | # Use a temporary variable to avoid issues if grep finds nothing
182 | PYTHON_STDOUT_CAPTURE="${{ steps.manage_stub_step.outputs.stdout }}"
183 | STATUS_LINE=$(echo "${PYTHON_STDOUT_CAPTURE}" | grep REPO_PROCESSED_STATUS= || true) # Prevent grep from failing the line if no match
184 |
185 | if [[ -n "${STATUS_LINE}" ]]; then # If STATUS_LINE is not empty
186 | PARSED_STATUS=${STATUS_LINE#*=}
187 | if [[ -n "${PARSED_STATUS}" ]]; then # If PARSED_STATUS is not empty after extraction
188 | FINAL_STATUS="${PARSED_STATUS}"
189 | fi
190 | elif [[ "${{ steps.manage_stub_step.outcome }}" == "failure" ]]; then
191 | FINAL_STATUS="script_failed_see_logs" # If script failed and we couldn't parse specific status
192 | fi
193 |
194 | ICON="ℹ️ Unknown" # Default icon
195 | if [[ "${{ steps.manage_stub_step.outcome }}" == "failure" ]]; then
196 | ICON="❌ Error"
197 | elif [[ "${{ steps.manage_stub_step.outcome }}" == "success" ]]; then
198 | # If successful, refine icon based on parsed status
199 | if [[ "${FINAL_STATUS}" == "skipped_"* ]]; then ICON="⚪ Skipped";
200 | elif [[ "${FINAL_STATUS}" == *"updated"* || "${FINAL_STATUS}" == *"created"* || "${FINAL_STATUS}" == *"removed"* ]]; then ICON="📝 Action";
201 | elif [[ "${FINAL_STATUS}" == "success_script_did_not_error" || "${FINAL_STATUS}" == "skipped_stub_up_to_date" || "${FINAL_STATUS}" == "skipped_permissive_no_stub" ]]; then ICON="✅ Success/NoOp";
202 | else ICON="✅ Success (Status: ${FINAL_STATUS})"; fi # Catch other success statuses
203 | fi
204 |
205 | echo "#### ${{ matrix.repository_full_name }}" >> $GITHUB_STEP_SUMMARY
206 | echo "- Parsed Status: ${FINAL_STATUS}" >> $GITHUB_STEP_SUMMARY # Use the determined FINAL_STATUS
207 | echo "- Step Outcome: ${ICON} (${{ steps.manage_stub_step.outcome }})" >> $GITHUB_STEP_SUMMARY
208 | echo "---" >> $GITHUB_STEP_SUMMARY
209 |
210 | summarize_run:
211 | name: Final Run Summary
212 | if: always()
213 | needs: [discover_repositories, process_repositories] # Removed build_licensee_image from needs here
214 | runs-on: ubuntu-latest
215 | steps:
216 | - name: Create final summary of the run
217 | run: |
218 | echo "### CLA Stub Management Full Run Summary" >> $GITHUB_STEP_SUMMARY
219 | echo "- Total Repositories Discovered for Processing: **${{ needs.discover_repositories.outputs.repository_count || 0 }}**" >> $GITHUB_STEP_SUMMARY
220 | # No separate build job status to report now, as it's part of each matrix job.
221 | # You could infer overall build success if all matrix jobs succeeded, but that's indirect.
222 |
223 | RESULT_MSG="ℹ️ **Overall Repository Processing Result:** Status - ${{ needs.process_repositories.result }}."
224 | if [[ "${{ needs.process_repositories.result }}" == "failure" ]]; then
225 | RESULT_MSG="⚠️ **Overall Repository Processing Result:** At least one repository failed processing. This could be due to image build, license check, or API errors."
226 | elif [[ "${{ needs.process_repositories.result }}" == "success" ]]; then
227 | RESULT_MSG="✅ **Overall Repository Processing Result:** All processed repositories completed without error."
228 | elif [[ "${{ needs.process_repositories.result }}" == "skipped" ]]; then
229 | RESULT_MSG="⚪ **Overall Repository Processing Result:** Processing job was skipped."
230 | fi
231 | echo "$RESULT_MSG Check individual 'Process Repository' job logs and their summaries for details." >> $GITHUB_STEP_SUMMARY
232 |
--------------------------------------------------------------------------------
/.github/workflows/old/license-cal-check.yml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.github/workflows/python_licensee_scan.yml:
--------------------------------------------------------------------------------
1 | name: Python + Licensee CLI Org Public License Scan
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | organization:
7 | description: 'GitHub organization name (e.g., "my-org")'
8 | required: true
9 | type: string
10 | github_token:
11 | description: 'Optional GitHub PAT. If empty for manual run, secrets.GITHUB_TOKEN will be used.'
12 | required: false
13 | type: string
14 | output_filename:
15 | description: 'Name of the output JSON file'
16 | required: false
17 | default: 'organization_public_licenses_licensee.json'
18 | type: string
19 | schedule:
20 | - cron: '0 2 * * 1' # Example: Run every Monday at 2 AM UTC
21 |
22 | jobs:
23 | scan_licenses_with_licensee_cli:
24 | runs-on: ubuntu-latest
25 | permissions:
26 | contents: read # For checkout and GITHUB_TOKEN to read public repo data & clone
27 |
28 | steps:
29 | - name: Checkout repository (to get the Python script)
30 | uses: actions/checkout@v4
31 |
32 | - name: Set up Python
33 | uses: actions/setup-python@v5
34 | with:
35 | python-version: '3.10'
36 |
37 | - name: Install Python dependencies
38 | run: pip install PyGithub
39 |
40 | - name: Set up Ruby and Bundler
41 | uses: ruby/setup-ruby@v1
42 | with:
43 | ruby-version: '3.1'
44 |
45 | - name: Install licensee CLI
46 | run: gem install licensee
47 |
48 | - name: Install/Ensure GitHub CLI (for auth setup)
49 | run: |
50 | echo "Installing/Ensuring GitHub CLI..."
51 | if ! type -p gh &>/dev/null; then
52 | echo "GitHub CLI not found, installing..."
53 | sudo apt-get update -qq && sudo apt-get install -y -qq curl
54 | curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
55 | && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
56 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
57 | && sudo apt-get update -qq \
58 | && sudo apt-get install -y -qq gh
59 | else
60 | echo "GitHub CLI already installed."
61 | fi
62 | echo "GitHub CLI version:"
63 | gh --version
64 | echo "--- End GitHub CLI Install ---"
65 |
66 | - name: Prepare environment variables for Python script
67 | id: prep_env
68 | run: |
69 | echo "--- Start Prepare Environment Variables ---"
70 | _ORG_SOURCE=""
71 | _TOKEN_SOURCE=""
72 | _OUTPUT_FILENAME_VAL=""
73 |
74 | if [[ "${{ github.event_name }}" == "schedule" ]]; then
75 | if [ -z "${{ secrets.ORG_NAME_FOR_SCAN }}" ]; then
76 | echo "Error: ORG_NAME_FOR_SCAN secret is not set for scheduled run."
77 | exit 1
78 | fi
79 | echo "ORGANIZATION_TO_SCAN=${{ secrets.ORG_NAME_FOR_SCAN }}" >> $GITHUB_ENV
80 | _ORG_SOURCE="secrets.ORG_NAME_FOR_SCAN"
81 |
82 | echo "GH_TOKEN_FOR_SCAN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
83 | _TOKEN_SOURCE="secrets.GITHUB_TOKEN (scheduled)"
84 |
85 | _OUTPUT_FILENAME_VAL="organization_public_licenses_licensee_scheduled.json"
86 | else # workflow_dispatch
87 | if [ -z "${{ github.event.inputs.organization }}" ]; then
88 | echo "Error: 'organization' input is not set for manual run."
89 | exit 1
90 | fi
91 | echo "ORGANIZATION_TO_SCAN=${{ github.event.inputs.organization }}" >> $GITHUB_ENV
92 | _ORG_SOURCE="inputs.organization"
93 |
94 | if [ -n "${{ github.event.inputs.github_token }}" ]; then
95 | echo "GH_TOKEN_FOR_SCAN=${{ github.event.inputs.github_token }}" >> $GITHUB_ENV
96 | _TOKEN_SOURCE="inputs.github_token (manual)"
97 | else
98 | echo "GH_TOKEN_FOR_SCAN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
99 | _TOKEN_SOURCE="secrets.GITHUB_TOKEN (manual fallback)"
100 | fi
101 |
102 | _OUTPUT_FILENAME_VAL="${{ github.event.inputs.output_filename }}"
103 | if [ -z "$_OUTPUT_FILENAME_VAL" ]; then # Use default if input is empty
104 | _OUTPUT_FILENAME_VAL="organization_public_licenses_licensee.json"
105 | fi
106 | fi
107 | echo "OUTPUT_FILENAME_TO_USE=$_OUTPUT_FILENAME_VAL" >> $GITHUB_ENV
108 | echo "Prepared Env Vars: ORG_FROM='$_ORG_SOURCE', TOKEN_FROM='$_TOKEN_SOURCE', OUTPUT_FILE='$_OUTPUT_FILENAME_VAL'"
109 | echo "Checking GITHUB_ENV content (first few lines):"
110 | head -n 5 $GITHUB_ENV || echo "GITHUB_ENV not found or empty."
111 | echo "--- End Prepare Environment Variables ---"
112 |
113 | - name: Authenticate GitHub CLI (for git clone)
114 | env:
115 | # This GH_TOKEN_FROM_WORKFLOW is the token we want to use.
116 | # We will pass it via stdin to gh auth login.
117 | GH_TOKEN_FROM_WORKFLOW: ${{ env.GH_TOKEN_FOR_SCAN }}
118 | run: |
119 | echo "--- Start Authenticate GitHub CLI ---"
120 | echo "Value of GH_TOKEN_FOR_SCAN (from previous step GITHUB_ENV): ${{ env.GH_TOKEN_FOR_SCAN }}"
121 | echo "Value of GH_TOKEN_FROM_WORKFLOW (this step's env): $GH_TOKEN_FROM_WORKFLOW"
122 |
123 | if [ -n "$GH_TOKEN_FROM_WORKFLOW" ]; then
124 | echo "Attempting to authenticate GitHub CLI with token (for git)..."
125 |
126 | echo "DEBUG: Current GH_TOKEN before unset: '${GH_TOKEN:-not set}'"
127 | # Temporarily unset GH_TOKEN for the gh auth login command itself,
128 | # so it doesn't complain about it already being set.
129 | # Pipe the token from our workflow variable into its stdin.
130 | if (unset GH_TOKEN; echo "Token is being piped to gh auth login" ; echo "$GH_TOKEN_FROM_WORKFLOW" | gh auth login --with-token --hostname github.com); then
131 | echo "gh auth login command completed successfully."
132 | else
133 | echo "ERROR: gh auth login command failed. Exit code: $?"
134 | # exit 1 # Optionally exit immediately
135 | fi
136 | echo "DEBUG: Current GH_TOKEN after gh auth login attempt: '${GH_TOKEN:-not set}'"
137 |
138 | echo "Attempting gh auth setup-git..."
139 | # gh auth setup-git should now pick up the token stored by gh auth login
140 | if gh auth setup-git --hostname github.com; then
141 | echo "gh auth setup-git command completed successfully."
142 | else
143 | echo "ERROR: gh auth setup-git command failed. Exit code: $?"
144 | # exit 1 # Optionally exit immediately
145 | fi
146 | echo "gh auth login and gh auth setup-git process finished."
147 | else
148 | echo "Warning: No GitHub token available (GH_TOKEN_FROM_WORKFLOW is empty)."
149 | echo "Public repo clones might work, but API access by Python will fail without a token."
150 | fi
151 |
152 | echo "Verifying gh auth status:"
153 | # Temporarily unset GH_TOKEN here too, so gh auth status checks stored creds
154 | (unset GH_TOKEN; gh auth status -h github.com) || echo "gh auth status indicated not logged in or failed to check."
155 |
156 | echo "Verifying git config for github.com:"
157 | git config --global --get-all http.https://github.com/.extraheader || echo "Git extraheader for github.com not found."
158 |
159 | echo "Listing relevant git config settings:"
160 | git config --global --list | grep -E 'http\.extraheader|credential\.helper' || echo "No relevant git config found."
161 | echo "--- End Authenticate GitHub CLI ---"
162 |
163 | - name: Run Python script with Licensee CLI
164 | env:
165 | ORGANIZATION_TO_SCAN: ${{ env.ORGANIZATION_TO_SCAN }}
166 | # Python script uses GH_TOKEN_FOR_SCAN for PyGithub
167 | GH_TOKEN_FOR_SCAN: ${{ env.GH_TOKEN_FOR_SCAN }}
168 | OUTPUT_FILENAME_TO_USE: ${{ env.OUTPUT_FILENAME_TO_USE }}
169 | # For debugging git within Python's subprocess:
170 | GIT_TRACE: "1"
171 | GIT_CURL_VERBOSE: "1"
172 | run: |
173 | echo "--- Start Run Python script ---"
174 | echo "Environment variables for Python script:"
175 | echo "ORGANIZATION_TO_SCAN: $ORGANIZATION_TO_SCAN"
176 | echo "OUTPUT_FILENAME_TO_USE: $OUTPUT_FILENAME_TO_USE"
177 | # GH_TOKEN_FOR_SCAN will be masked by GitHub Actions if it's a secret
178 |
179 | python .github/scripts/scan_org_licenses_licensee.py # Ensure this path is correct
180 | echo "--- End Run Python script ---"
181 |
182 |
183 | - name: Upload license report
184 | if: always()
185 | uses: actions/upload-artifact@v4
186 | with:
187 | name: python-licensee-cli-report
188 | path: ${{ env.OUTPUT_FILENAME_TO_USE }}
189 | if-no-files-found: warn
190 |
--------------------------------------------------------------------------------
/.github/workflows/reusable-cla-check.yml:
--------------------------------------------------------------------------------
1 | # File: .github/.github/workflows/reusable-cla-check.yml
2 | # Purpose: Centralized, reusable workflow for performing the CLA check.
3 | # Signatures are read from and committed to a CSV file within this .github repository.
4 |
5 | name: Reusable CLA Check (Repo Signatures)
6 |
7 | on:
8 |
9 | workflow_call:
10 | secrets:
11 | # This PAT is used by cla-assistant/github-action for:
12 | # 1. PR interactions (comments, labels, statuses) on the TARGET repository.
13 | # 2. Reading and committing signatures to the CLA.csv file in THIS .github repository.
14 | # It requires:
15 | # - Pull requests: R/W, Issues: R/W, Commit statuses: R/W (for all target repos).
16 | # - Contents: R/W (for THIS .github repository to manage CLA.csv).
17 | CONTRIBUTOR_ASSISTANT_PAT:
18 | description: 'PAT for CLA Assistant Lite (PR interaction on target repos AND signature commits to this .github repo).'
19 | required: true
20 | inputs:
21 | # URL or path to the CLA document text.
22 | # If a path, it's relative to the root of THIS .github repository after checkout.
23 | cla_document_url:
24 | description: 'URL or path to the CLA document text.'
25 | required: false # Made optional, can default
26 | type: string
27 | default: 'https://vmware.github.io/cla-docs/GenericCLA.html' # Dynamic default
28 | # default: 'https://github.com/${{ github.repository_owner }}/.github/blob/main/.github/CONTRIBUTOR_LICENSE_AGREEMENT.md' # Dynamic default
29 | # Path to the CSV signature file within THIS .github repository.
30 | signature_file_path:
31 | description: 'Path to the CSV signature file within this .github repository.'
32 | required: false
33 | type: string
34 | default: 'signatures/CLA.json' # Default path
35 | # Branch in THIS .github repository where signatures are stored and committed.
36 | signature_branch:
37 | description: 'Branch in this .github repository where signatures are stored and committed.'
38 | required: false
39 | type: string
40 | default: 'main' # Default branch (e.g., main or your .github repo's default)
41 |
42 | # Permissions this reusable workflow's GITHUB_TOKEN needs in the context of the CALLER's repository (target repo).
43 | # cla-assistant/github-action primarily uses its own CONTRIBUTOR_ASSISTANT_PAT for its operations.
44 | permissions:
45 | pull-requests: write # For commenting/labeling on the target PR.
46 | issues: write # For commenting on issues on the target PR.
47 | statuses: write # For setting commit statuses on the target PR.
48 | # Contents: write on THIS .github repo is handled by the CONTRIBUTOR_ASSISTANT_PAT PAT,
49 | # not this workflow's GITHUB_TOKEN.
50 |
51 | jobs:
52 | cla_check:
53 | runs-on: ubuntu-latest
54 | # The 'if' condition to run this job is now primarily handled by the CALLER (the stub workflow).
55 | # Run only for relevant PR actions.
56 | steps:
57 | # Checkout THIS .github repository.
58 | # This makes the signature file (e.g., .github/signatures/CLA.csv) and potentially the
59 | # CLA document (if stored locally) available to the cla-assistant-lite action.
60 | - name: Checkout .github repository (for signature file access)
61 | uses: actions/checkout@v4.1.1
62 | with:
63 | # Explicitly checkout THIS .github repository where the workflow runs and signatures are stored.
64 | repository: ${{ github.repository }} # e.g., YOUR_ORG/.github
65 | # Checkout the branch where the signature file (CLA.csv) is located and where new signatures will be committed.
66 | ref: ${{ inputs.signature_branch }}
67 | # The default GITHUB_TOKEN of this job is sufficient for checking out its own repository.
68 |
69 | - name: CLA Assistant Lite (Signatures in Repo)
70 |
71 | # Use the official cla-assistant/github-action. Pin to a specific version.
72 | uses: contributor-assistant/github-action@v2.6.1
73 | env:
74 | # Provide the dedicated PAT. This PAT needs permissions for:
75 | # 1. PR interactions (comments, labels, statuses) on the *target repository* (where the PR was opened).
76 | # 2. Contents Read & Write permissions on *this .github repository* to read/commit CLA.csv.
77 | GITHUB_TOKEN: ${{ secrets.CONTRIBUTOR_ASSISTANT_PAT }}
78 | # PERSONAL_ACCESS_TOKEN is REQUIRED for writing signatures to a remote repository.
79 | # We can use the same PAT if it has the necessary permissions for the .github repo.
80 | PERSONAL_ACCESS_TOKEN: ${{ secrets.CONTRIBUTOR_ASSISTANT_PAT }}
81 | with:
82 | # --- Configuration for Centralized Signatures ---
83 | # Organization where the .github repository (hosting signatures) resides.
84 | remote-organization-name: ${{ github.repository_owner }} # e.g., 'vmware'
85 | # Name of the repository hosting the signatures (i.e., '.github').
86 | remote-repository-name: '.github' # The name of your .github repository
87 |
88 | # Path to the signature CSV file within the checked-out .github repository.
89 | path-to-signatures: ${{ inputs.signature_file_path }}
90 | # Path or URL to the CLA document text.
91 | path-to-document: ${{ inputs.cla_document_url }}
92 | # Branch in THIS .github repository where new signatures should be committed.
93 | branch: ${{ inputs.signature_branch }}
94 | # List of users/bots to ignore for CLA checks.
95 | allowlist: bot*,dependabot[bot],github-actions[bot],renovate[bot]
96 |
97 | # --- Optional configurations for cla-assistant-lite ---
98 | # Custom commit message when a new signature is added to CLA.csv.
99 | # Uses GitHub context variables available to cla-assistant-lite.
100 | create-file-commit-message: 'chore(CLA): Add signature for @${{ github.event.sender.login }} for PR #${{ github.event.pull_request.number }} on ${{ github.event.repository.full_name }}'
101 | # This tells the action what comment triggers a signature.
102 | # The README image you provided had 'custom-pr-sign-comment'.
103 | custom-pr-sign-comment: "I have read the CLA Document and I hereby sign the CLA"
104 |
105 | # User to attribute commits to if the action commits signatures.
106 | # github-actions[bot] is a good default if you want to distinguish these commits.
107 | # commit-author-name: 'CLA Automation'
108 | # commit-author-email: 'cla-bot@users.noreply.github.com' # Or a dedicated bot user email
109 |
110 | # Lock PR from non-members until CLA is signed
111 | # lock-pullrequest-after: true
112 | # Custom message when CLA is required
113 | # sigRequiredComment: 'Thanks for your contribution! Please sign our CLA to proceed.'
114 |
--------------------------------------------------------------------------------
/.github/workflows/scan-licenses.yml:
--------------------------------------------------------------------------------
1 | name: Organization Public License Scan
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | organization:
7 | description: 'GitHub organization name (e.g., "my-org")'
8 | required: true
9 | type: string
10 | github_token:
11 | description: 'Optional GitHub PAT. If empty for a manual run, secrets.GITHUB_TOKEN will be used.'
12 | required: false
13 | type: string
14 | output_filename:
15 | description: 'Name of the output JSON file'
16 | required: false
17 | default: 'organization_public_licenses.json'
18 | type: string
19 | schedule:
20 | # Example: Run every Monday at 2 AM UTC
21 | - cron: '0 2 * * 1'
22 |
23 | jobs:
24 | scan_licenses:
25 | runs-on: ubuntu-latest
26 | permissions:
27 | contents: read # For checkout
28 |
29 | steps:
30 | - name: Checkout code (optional)
31 | uses: actions/checkout@v4
32 |
33 | - name: Set up Ruby
34 | uses: ruby/setup-ruby@v1
35 | with:
36 | ruby-version: '3.1'
37 | bundler-cache: true
38 |
39 | - name: Install licensee
40 | run: gem install licensee
41 |
42 | - name: Install jq
43 | run: sudo apt-get update && sudo apt-get install -y jq
44 |
45 | - name: Install/Ensure GitHub CLI
46 | run: |
47 | if ! type -p gh &>/dev/null; then
48 | echo "GitHub CLI not found, installing..."
49 | sudo apt-get update
50 | sudo apt-get install -y curl
51 | curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
52 | && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
53 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
54 | && sudo apt update \
55 | && sudo apt install -y gh
56 | else
57 | echo "GitHub CLI already installed."
58 | fi
59 | gh --version
60 |
61 | - name: Prepare environment variables
62 | id: prep_env
63 | run: |
64 | if [[ "${{ github.event_name }}" == "schedule" ]]; then
65 | if [ -z "${{ secrets.ORG_NAME_FOR_SCAN }}" ]; then
66 | echo "Error: ORG_NAME_FOR_SCAN secret is not set for scheduled run."
67 | exit 1
68 | fi
69 | echo "ORGANIZATION_TO_SCAN=${{ secrets.ORG_NAME_FOR_SCAN }}" >> $GITHUB_ENV
70 | echo "GH_TOKEN_FOR_SCAN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
71 | echo "OUTPUT_FILENAME_TO_USE=organization_public_licenses_scheduled.json" >> $GITHUB_ENV
72 | echo "Scheduled run: Using secrets.ORG_NAME_FOR_SCAN and secrets.GITHUB_TOKEN."
73 | else # workflow_dispatch
74 | if [ -z "${{ github.event.inputs.organization }}" ]; then
75 | echo "Error: 'organization' input is not set for manual run."
76 | exit 1
77 | fi
78 | echo "ORGANIZATION_TO_SCAN=${{ github.event.inputs.organization }}" >> $GITHUB_ENV
79 |
80 | if [ -n "${{ github.event.inputs.github_token }}" ]; then
81 | echo "Manual run: Using provided github_token input."
82 | echo "GH_TOKEN_FOR_SCAN=${{ github.event.inputs.github_token }}" >> $GITHUB_ENV
83 | else
84 | echo "Manual run: github_token input is empty. Using secrets.GITHUB_TOKEN as fallback."
85 | echo "GH_TOKEN_FOR_SCAN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
86 | fi
87 | echo "OUTPUT_FILENAME_TO_USE=${{ github.event.inputs.output_filename }}" >> $GITHUB_ENV
88 | echo "Manual run: Using inputs for organization and output_filename."
89 | fi
90 | # Mask the token value if it's set and passed to the script via GH_TOKEN_FOR_SCAN
91 | # Note: $GITHUB_ENV variables are automatically masked if they look like secrets.
92 | # This explicit masking is for the variable if it were used directly in `run` scripts.
93 | # For GH_TOKEN_FOR_SCAN being put into GITHUB_ENV, it should be auto-masked by Actions.
94 | # However, if we were to echo it directly:
95 | # TOKEN_VALUE_TO_MASK=$(echo "$GH_TOKEN_FOR_SCAN" | sed 's/./*/g') # Example of how you might get it
96 | # echo "::add-mask::${TOKEN_VALUE_TO_MASK}" # This isn't quite right as we don't have it here directly
97 | # The best way is to rely on Actions auto-masking for values from secrets and GITHUB_ENV.
98 | # If GH_TOKEN_FOR_SCAN is set, it will be used by the next step.
99 | echo "Token for scan will be (masked if secret): $GH_TOKEN_FOR_SCAN"
100 |
101 |
102 | - name: Authenticate GitHub CLI and Git
103 | env:
104 | GH_TOKEN: ${{ env.GH_TOKEN_FOR_SCAN }} # This correctly picks up from GITHUB_ENV
105 | run: |
106 | if [ -z "$ORGANIZATION_TO_SCAN" ]; then
107 | echo "Error: Organization name (ORGANIZATION_TO_SCAN) is not available."
108 | exit 1
109 | fi
110 |
111 | if [ -n "$GH_TOKEN" ]; then
112 | echo "Authenticating GitHub CLI and Git with the determined token."
113 | echo "$GH_TOKEN" | gh auth login --with-token
114 | gh auth setup-git
115 | else
116 | echo "Warning: No GitHub token available (GH_TOKEN is empty). Proceeding with unauthenticated access (lowest rate limits)."
117 | git config --global credential.helper ''
118 | fi
119 |
120 | - name: Scan public repositories and generate report
121 | run: |
122 | set -e
123 | set -o pipefail
124 |
125 | echo "Scanning public repositories in organization: $ORGANIZATION_TO_SCAN"
126 | echo "Output file will be: $OUTPUT_FILENAME_TO_USE"
127 |
128 | TEMP_LICENSE_FILE="license_lines.temp.jsonl"
129 | FINAL_OUTPUT_FILE="$OUTPUT_FILENAME_TO_USE"
130 | > "$TEMP_LICENSE_FILE"
131 |
132 | echo "Fetching public repository list for $ORGANIZATION_TO_SCAN..."
133 | repo_names_json=$(gh repo list "$ORGANIZATION_TO_SCAN" --visibility public --limit 2000 --json name --jq '.[].name')
134 |
135 | if [ -z "$repo_names_json" ]; then
136 | echo "No public repositories found in organization $ORGANIZATION_TO_SCAN or failed to list them."
137 | echo "[]" > "$FINAL_OUTPUT_FILE"
138 | echo "Workflow finished: No public repositories to scan."
139 | exit 0
140 | fi
141 |
142 | MAX_RETRIES=3
143 | RETRY_DELAY_SECONDS=10
144 | mapfile -t repo_array < <(echo "$repo_names_json")
145 |
146 | for repo_name in "${repo_array[@]}"; do
147 | if [ -z "$repo_name" ]; then
148 | continue
149 | fi
150 |
151 | repo_full_name="$ORGANIZATION_TO_SCAN/$repo_name"
152 | echo "-----------------------------------------------------"
153 | echo "Processing repository: $repo_full_name"
154 | CLONE_DIR=$(mktemp -d -t "repo_${repo_name//\//_}_XXXXXX")
155 | echo "Cloning to temporary directory: $CLONE_DIR"
156 | current_attempt=1
157 | license_id="ERROR_PROCESSING"
158 |
159 | while [ $current_attempt -le $MAX_RETRIES ]; do
160 | echo "Attempt $current_attempt/$MAX_RETRIES to clone and analyze $repo_full_name..."
161 | if GIT_TERMINAL_PROMPT=0 git clone --depth 1 --quiet "https://github.com/$repo_full_name.git" "$CLONE_DIR"; then
162 | echo "Clone successful."
163 | cd "$CLONE_DIR"
164 | license_output=$(licensee detect --json . 2>/dev/null || echo "LICENSEE_CLI_ERROR")
165 | cd ..
166 |
167 | if [[ "$license_output" == "LICENSEE_CLI_ERROR" ]]; then
168 | license_id="LICENSEE_CLI_ERROR"
169 | echo "Licensee CLI failed for $repo_full_name."
170 | elif [[ "$license_output" == "null" ]] || [[ -z "$license_output" ]]; then
171 | license_id="NONE_FOUND"
172 | echo "No license found by licensee in $repo_full_name."
173 | else
174 | license_id_raw=$(echo "$license_output" | jq -r '.matched_license.spdx_id // .matched_license.name // "UNKNOWN_OR_NO_MATCH"')
175 | # JQ's // operator already handles null, so extra check for "null" string is less critical but fine
176 | if [[ "$license_id_raw" == "null" ]] || [[ -z "$license_id_raw" ]] || [[ "$license_id_raw" == "UNKNOWN_OR_NO_MATCH" && "$license_output" != *"UNKNOWN_OR_NO_MATCH"* ]]; then
177 | license_id="UNKNOWN_OR_NO_MATCH" # Ensure this if jq itself returned the fallback literal
178 | echo "Licensee ran, but license SPDX ID or name was effectively null/empty in $repo_full_name."
179 | else
180 | license_id="$license_id_raw"
181 | echo "License found for $repo_full_name: $license_id"
182 | fi
183 | fi
184 | break
185 | else
186 | echo "Clone failed for $repo_full_name (attempt $current_attempt)."
187 | if [ $current_attempt -lt $MAX_RETRIES ]; then
188 | echo "Retrying in $RETRY_DELAY_SECONDS seconds..."
189 | sleep $RETRY_DELAY_SECONDS
190 | else
191 | echo "Max retries reached for $repo_full_name. Marking as clone error."
192 | license_id="ERROR_CLONING"
193 | fi
194 | fi
195 | current_attempt=$((current_attempt + 1))
196 | done
197 | jq -n --arg repo_name "$repo_name" --arg license_id "$license_id" \
198 | '{ "repository_name": $repo_name, "license": $license_id }' >> "$TEMP_LICENSE_FILE"
199 | rm -rf "$CLONE_DIR"
200 | echo "Cleaned up $CLONE_DIR."
201 | done
202 |
203 | if [ -s "$TEMP_LICENSE_FILE" ]; then
204 | jq -s '.' "$TEMP_LICENSE_FILE" > "$FINAL_OUTPUT_FILE"
205 | else
206 | echo "[]" > "$FINAL_OUTPUT_FILE"
207 | fi
208 | rm "$TEMP_LICENSE_FILE"
209 |
210 | echo "-----------------------------------------------------"
211 | echo "Public license report generated: $FINAL_OUTPUT_FILE"
212 | echo "Content of $FINAL_OUTPUT_FILE:"
213 | cat "$FINAL_OUTPUT_FILE"
214 |
215 | - name: Upload license report
216 | uses: actions/upload-artifact@v4
217 | with:
218 | name: public-license-report
219 | path: ${{ env.OUTPUT_FILENAME_TO_USE }}
220 |
--------------------------------------------------------------------------------
/.github/workflows/secrets-scanning-report.yml:
--------------------------------------------------------------------------------
1 | name: Centralized Secret Scanning Report
2 |
3 | on:
4 | workflow_dispatch:
5 | inputs:
6 | include_inactive:
7 | description: 'Include inactive alerts in report'
8 | required: false
9 | type: boolean
10 | default: false
11 | max_workers:
12 | description: 'Maximum number of concurrent workers'
13 | required: false
14 | type: number
15 | default: 10
16 | log_level:
17 | description: 'Logging level'
18 | required: false
19 | type: choice
20 | options:
21 | - INFO
22 | - DEBUG
23 | - WARNING
24 | - ERROR
25 | default: 'INFO'
26 | alert_threshold:
27 | description: 'Number of active alerts to trigger issue creation'
28 | required: false
29 | type: number
30 | default: 10
31 | schedule:
32 | - cron: '0 0 * * 1'
33 |
34 | permissions:
35 | security-events: read
36 | contents: write
37 | actions: write
38 | issues: write
39 |
40 | jobs:
41 | generate-report:
42 | runs-on: ubuntu-latest
43 |
44 | steps:
45 | - name: Checkout .github repo
46 | uses: actions/checkout@v4
47 | with:
48 | repository: ${{ github.repository_owner }}/.github
49 | ref: main
50 | token: ${{ secrets.SECRET_SCANNING_TOKEN }}
51 |
52 | - name: Set up Python
53 | uses: actions/setup-python@v5
54 | with:
55 | python-version: '3.11'
56 | cache: 'pip'
57 | cache-dependency-path: scripts/requirements.txt # Now correct
58 |
59 | - name: Install dependencies
60 | run: |
61 | python -m pip install --upgrade pip
62 | pip install -r scripts/requirements.txt # Now correct
63 |
64 | - name: Generate timestamp
65 | id: timestamp
66 | run: echo "timestamp=$(date +%Y%m%d_%H%M%S)" >> $GITHUB_OUTPUT
67 |
68 | - name: Generate Secret Report
69 | id: generate-report
70 | env:
71 | GITHUB_TOKEN: ${{ secrets.SECRET_SCANNING_TOKEN }}
72 | ORGANIZATION: ${{ github.repository_owner }}
73 | REPORT_FILE: "secret_report_${{ steps.timestamp.outputs.timestamp }}.csv"
74 | run: |
75 | # Create the reports directory (now in the correct location)
76 | mkdir -p reports
77 |
78 | # Construct the command string explicitly (now simpler paths)
79 | COMMAND="python scripts/github_secret_scanner.py \
80 | --org $ORGANIZATION \
81 | --token $GITHUB_TOKEN \
82 | --output reports/$REPORT_FILE \
83 | --log-level ${{ inputs.log_level || 'INFO' }} \
84 | --max-workers ${{ inputs.max_workers || 10 }} \
85 | --max-retries 3"
86 |
87 | # Add the conditional --include-inactive flag
88 | if [[ "${{ inputs.include_inactive }}" == "true" ]]; then
89 | COMMAND="$COMMAND --include-inactive"
90 | fi
91 |
92 | # Execute the command
93 | $COMMAND
94 |
95 | echo "report_path=reports/$REPORT_FILE" >> $GITHUB_OUTPUT
96 |
97 | - name: Check for No Repositories
98 | id: check-repos
99 | if: success()
100 | run: |
101 | if grep -q "__NO_REPOS__" ${{ steps.generate-report.outputs.report_path }}/../output.txt; then
102 | echo "No repositories found in the organization. Exiting."
103 | exit 1
104 | fi
105 |
106 | - name: Process report statistics (inline)
107 | id: stats
108 | if: success() && steps.check-repos.outcome == 'success'
109 | run: |
110 | STATS=$(grep "__STATS_START__" ${{ steps.generate-report.outputs.report_path }}/../output.txt | sed 's/__STATS_START__//' | sed 's/__STATS_END__//')
111 | echo "total_alerts=$(echo $STATS | cut -d',' -f1 | cut -d'=' -f2)" >> $GITHUB_OUTPUT
112 | echo "active_alerts=$(echo $STATS | cut -d',' -f2 | cut -d'=' -f2)" >> $GITHUB_OUTPUT
113 | echo "inactive_alerts=$(echo $STATS | cut -d',' -f3 | cut -d'=' -f2)" >> $GITHUB_OUTPUT
114 | echo "Total alerts found: $(echo $STATS | cut -d',' -f1 | cut -d'=' -f2)"
115 | echo "Active alerts: $(echo $STATS | cut -d',' -f2 | cut -d'=' -f2)"
116 | echo "Inactive alerts: $(echo $STATS | cut -d',' -f3 | cut -d'=' -f2)"
117 |
118 | - name: Create summary issue (using github-script)
119 | if: success() && steps.check-repos.outcome == 'success' && steps.stats.outputs.active_alerts > inputs.alert_threshold
120 | uses: actions/github-script@v7
121 | with:
122 | script: |
123 | const stats = {
124 | total: '${{ steps.stats.outputs.total_alerts }}',
125 | active: '${{ steps.stats.outputs.active_alerts }}',
126 | inactive: '${{ steps.stats.outputs.inactive_alerts }}',
127 | };
128 |
129 | const now = new Date();
130 | const formattedDate = now.toLocaleDateString('en-US', {
131 | year: 'numeric',
132 | month: 'long',
133 | day: 'numeric'
134 | });
135 |
136 | const body = `
137 | # Secret Scanning Report Summary
138 |
139 | Report generated on: ${now.toISOString()}
140 |
141 | ## Statistics
142 | - Total alerts analyzed: ${stats.total}
143 | - Active alerts found: ${stats.active}
144 | - Inactive alerts found: ${stats.inactive}
145 |
146 | ## Details
147 | - Report artifact: [Download report](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
148 | - Workflow run: [View details](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
149 |
150 | ## Configuration
151 | - Include inactive alerts: ${{ inputs.include_inactive || 'false' }}
152 | - Max workers: ${{ inputs.max_workers || '10' }}
153 | - Log level: ${{ inputs.log_level || 'INFO' }}
154 | - Alert threshold: ${{ inputs.alert_threshold || '10'}}
155 | `;
156 |
157 | await github.rest.issues.create({
158 | owner: context.repo.owner,
159 | repo: context.repo.repo,
160 | title: \`📊 Secret Scanning Report - \${formattedDate}\`,
161 | body: body,
162 | labels: ['secret-scanning', 'report']
163 | });
164 |
165 | - name: Commit and Push Report
166 | if: success() && steps.check-repos.outcome == 'success'
167 | uses: stefanzweifel/git-auto-commit-action@v5
168 | with:
169 | commit_message: "Add secret scanning report: ${{ steps.timestamp.outputs.timestamp }}"
170 | repository: ./ # Commit to the root of the checked-out repo
171 | file_pattern: reports/*.csv
172 | commit_user_name: GitHub Actions
173 | commit_user_email: actions@github.com
174 | commit_author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
175 | push_options: '--force'
176 | token: ${{ secrets.SECRET_SCANNING_TOKEN }} # Use the PAT with write access!
177 |
178 | - name: Notify on failure
179 | if: failure()
180 | uses: actions/github-script@v7
181 | with:
182 | script: |
183 | const body = `
184 | # 🚨 Secret Scanning Report Generation Failed
185 |
186 | Workflow run failed at ${new Date().toISOString()}
187 |
188 | ## Details
189 | - Run ID: \`${context.runId}\`
190 | - Trigger: ${context.eventName}
191 | - Actor: @${context.actor}
192 |
193 | ## Links
194 | - [View run details](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
195 | - [View workflow file](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/blob/main/.github/workflows/secret-scanning-report.yml)
196 |
197 | Please check the workflow logs for detailed error information.
198 | `;
199 |
200 | await github.rest.issues.create({
201 | owner: context.repo.owner,
202 | repo: context.repo.repo,
203 | title: '🚨 Secret Scanning Report Generation Failed',
204 | body: body,
205 | labels: ['secret-scanning', 'failed']
206 | });
207 |
208 | - name: Clean up
209 | if: always()
210 | run: |
211 | echo "No clean up required."
212 |
213 | concurrency:
214 | group: ${{ github.workflow }}-${{ github.ref }}
215 | cancel-in-progress: true
216 |
217 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual
10 | identity and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the overall
26 | community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or advances of
31 | any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email address,
35 | without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official email address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | [opensource@broadcom.com][enforcement]. All complaints will be reviewed and
64 | investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series of
86 | actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or permanent
93 | ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within the
113 | community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][cc], version
118 | [v2.1][cc-v2.1].
119 |
120 | Community Impact Guidelines were inspired by Mozilla's
121 | [Code of Conduct Enforcement][mozilla-coce] consequence ladder.
122 |
123 | For answers to common questions about this code of conduct, please refer to the
124 | [Frequently Asked Questions][cc-faq]. Translations are available in
125 | [additional languages][cc-translations].
126 |
127 | [cc]: https://www.contributor-covenant.org
128 | [cc-faq]: https://www.contributor-covenant.org/faq/
129 | [cc-translations]: https://www.contributor-covenant.org/translations/
130 | [cc-v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/
131 | [enforcement]: mailto:opensource@broadcom.com?subject=Open%20Source%20Code%20of%20Conduct
132 | [mozilla-coce]: https://github.com/mozilla/inclusion/
133 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to {{ project }}
2 |
3 | _NOTE: This is a template document that requires editing before it is ready to use!_
4 |
5 | We welcome contributions from the community and first want to thank you for taking the time to contribute!
6 |
7 | Please familiarize yourself with the [Code of Conduct](https://github.com/vmware/.github/blob/main/CODE_OF_CONDUCT.md) before contributing.
8 |
9 | _TO BE EDITED: Depending on the open source license that governs the project, leave only one of the options below:_
10 |
11 | * _DCO: Before you start working with {{ project }}, please read our [Developer Certificate of Origin](https://cla.vmware.com/dco). All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch._
12 | * _CLA: Before you start working with {{ project }}, please read and sign our Contributor License Agreement [CLA](https://cla.vmware.com/cla/1/preview). If you wish to contribute code and you have not signed our contributor license agreement (CLA), our bot will update the issue when you open a Pull Request. For any questions about the CLA process, please refer to our [FAQ]([https://cla.vmware.com/faq](https://cla.vmware.com/faq))._
13 |
14 | ## Ways to contribute
15 |
16 | We welcome many different types of contributions and not all of them need a Pull request. Contributions may include:
17 |
18 | * New features and proposals
19 | * Documentation
20 | * Bug fixes
21 | * Issue Triage
22 | * Answering questions and giving feedback
23 | * Helping to onboard new contributors
24 | * Other related activities
25 |
26 | ## Getting started
27 |
28 | _TO BE EDITED: This section explains how to build the project from source, including Development Environment Setup, Build, Run and Test._
29 |
30 | _Provide information about how someone can find your project, get set up, build the code, test it, and submit a pull request successfully without having to ask any questions. Also include common errors people run into, or useful scripts they should run._
31 |
32 | _List any tests that the contributor should run / or testing processes to follow before submitting. Describe any automated and manual checks performed by reviewers._
33 |
34 |
35 | ## Contribution Flow
36 |
37 | This is a rough outline of what a contributor's workflow looks like:
38 |
39 | * Make a fork of the repository within your GitHub account
40 | * Create a topic branch in your fork from where you want to base your work
41 | * Make commits of logical units
42 | * Make sure your commit messages are with the proper format, quality and descriptiveness (see below)
43 | * Push your changes to the topic branch in your fork
44 | * Create a pull request containing that commit
45 |
46 | We follow the GitHub workflow and you can find more details on the [GitHub flow documentation](https://docs.github.com/en/get-started/quickstart/github-flow).
47 |
48 | Before submitting your pull request, we advise you to use the following:
49 |
50 |
51 | ### Pull Request Checklist
52 |
53 | 1. Check if your code changes will pass both code linting checks and unit tests.
54 | 2. Ensure your commit messages are descriptive. We follow the conventions on [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/). Be sure to include any related GitHub issue references in the commit message. See [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues and commits.
55 | 3. Check the commits and commits messages and ensure they are free from typos.
56 |
57 | ## Reporting Bugs and Creating Issues
58 |
59 | For specifics on what to include in your report, please follow the guidelines in the issue and pull request templates when available.
60 |
61 | _TO BE EDITED: Add additional information if needed._
62 |
63 |
64 | ## Ask for Help
65 |
66 | _TO BE EDITED: Provide information about the channels you use to communicate (i.e. Slack, IRC, Discord, etc)_
67 |
68 | The best way to reach us with a question when contributing is to ask on:
69 |
70 | * The original GitHub issue
71 | * The developer mailing list
72 | * Our Slack channel
73 |
74 |
75 | ## Additional Resources
76 |
77 | _Optional_
78 |
79 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | © Broadcom. All Rights Reserved.
2 | The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
3 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Release Process
2 |
3 | The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues.
4 |
5 |
6 | ## Supported Versions
7 |
8 | For a list of support versions that this project will potentially create security fixes for, please refer to the Releases page on this project's GitHub and/or project related documentation on release cadence and support.
9 |
10 |
11 | ## Reporting a Vulnerability - Private Disclosure Process
12 |
13 | Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to this project privately, to minimize attacks against current users before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project.
14 |
15 | If you know of a publicly disclosed security vulnerability for this project, please **IMMEDIATELY** contact the maintainers of this project privately. The use of encrypted email is encouraged.
16 |
17 |
18 | **IMPORTANT: Do not file public issues on GitHub for security vulnerabilities**
19 |
20 | To report a vulnerability or a security-related issue, please contact the maintainers with enough details through one of the following channels:
21 | * Directly via their individual email addresses
22 | * Open a [GitHub Security Advisory](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability). This allows for anyone to report security vulnerabilities directly and privately to the maintainers via GitHub. Note that this option may not be present for every repository.
23 |
24 | The report will be fielded by the maintainers who have committer and release permissions. Feedback will be sent within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime.
25 |
26 | Do not report non-security-impacting bugs through this channel. Use GitHub issues for all non-security-impacting bugs.
27 |
28 |
29 | ## Proposed Report Content
30 |
31 | Provide a descriptive title and in the description of the report include the following information:
32 |
33 | * Basic identity information, such as your name and your affiliation or company.
34 | * Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and logs are all helpful to us).
35 | * Description of the effects of the vulnerability on this project and the related hardware and software configurations, so that the maintainers can reproduce it.
36 | * How the vulnerability affects this project's usage and an estimation of the attack surface, if there is one.
37 | * List other projects or dependencies that were used in conjunction with this project to produce the vulnerability.
38 |
39 |
40 | ## When to report a vulnerability
41 |
42 | * When you think this project has a potential security vulnerability.
43 | * When you suspect a potential vulnerability but you are unsure that it impacts this project.
44 | * When you know of or suspect a potential vulnerability on another project that is used by this project.
45 |
46 |
47 | ## Patch, Release, and Disclosure
48 |
49 | The maintainers will respond to vulnerability reports as follows:
50 |
51 | 1. The maintainers will investigate the vulnerability and determine its effects and criticality.
52 | 2. If the issue is not deemed to be a vulnerability, the maintainers will follow up with a detailed reason for rejection.
53 | 3. The maintainers will initiate a conversation with the reporter within 3 business days.
54 | 4. If a vulnerability is acknowledged and the timeline for a fix is determined, the maintainers will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out.
55 | 5. The maintainers will also create a [Security Advisory](https://docs.github.com/en/code-security/repository-security-advisories/publishing-a-repository-security-advisory) using the [CVSS Calculator](https://www.first.org/cvss/calculator/3.0), if it is not created yet. The maintainers make the final call on the calculated CVSS; it is better to move quickly than making the CVSS perfect. Issues may also be reported to [Mitre](https://cve.mitre.org/) using this [scoring calculator](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The draft advisory will initially be set to private.
56 | 6. The maintainers will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix.
57 | 7. Once the fix is confirmed, the maintainers will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases.
58 |
59 |
60 | ## Public Disclosure Process
61 |
62 | The maintainers publish the public advisory to this project's community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog, and other channels will assist in educating the project's users and rolling out the patched release to affected users.
63 |
64 | The maintainers will also publish any mitigating steps users can take until the fix can be applied to their instances. This project's distributors will handle creating and publishing their own security advisories.
65 |
66 |
67 | ## Confidentiality, integrity and availability
68 |
69 | We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns. Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern. The maintainer team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner.
70 |
71 | Note that we do not currently consider the default settings for this project to be secure-by-default. It is necessary for operators to explicitly configure settings, role based access control, and other resource related features in this project to provide a hardened environment. We will not act on any security disclosure that relates to a lack of safe defaults. Over time, we will work towards improved safe-by-default configuration, taking into account backwards compatibility.
72 |
--------------------------------------------------------------------------------
/actions/check-license/action.yml:
--------------------------------------------------------------------------------
1 | name: License Check
2 | description: Fuzzy license matching with GPL exclusion
3 | runs:
4 | using: composite
5 | steps:
6 | - name: Setup Python
7 | uses: actions/setup-python@v4
8 | with:
9 | python-version: '3.x'
10 |
11 | - name: Install dependencies
12 | run: pip install PyGithub python-Levenshtein
13 |
14 | - name: Fuzzy License Check
15 | shell: python
16 | env:
17 | ORG_TOKEN: ${{ secrets.ORG_TOKEN }}
18 | ENABLED_REPOS: ${{ vars.ENABLED_REPOS || '[]' }}
19 | EXCLUDED_REPOS: ${{ vars.EXCLUDED_REPOS || '[]' }}
20 | CURRENT_REPO: ${{ github.repository }}
21 | run: |
22 | import os
23 | import re
24 | import json
25 | from difflib import SequenceMatcher
26 | from github import Github, GithubException
27 |
28 | # Check if the repository is enabled or excluded
29 | enabled_repos = json.loads(os.environ['ENABLED_REPOS'])
30 | excluded_repos = json.loads(os.environ['EXCLUDED_REPOS'])
31 | current_repo = os.environ['CURRENT_REPO']
32 |
33 | if enabled_repos and current_repo not in enabled_repos:
34 | print(f"Skipping repository {current_repo} (not in enabled list)")
35 | with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
36 | fh.write('license_status=skipped')
37 | exit(0)
38 |
39 | if current_repo in excluded_repos:
40 | print(f"Skipping repository {current_repo} (excluded)")
41 | with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
42 | fh.write('license_status=skipped')
43 | exit(0)
44 |
45 | # Configuration
46 | EXCLUDED_KEYWORDS = {
47 | 'gpl', 'gnu', 'general', 'public', 'version', '2', '3',
48 | 'agpl', 'lgpl', 'lesser', 'copying', 'affero', 'copyleft',
49 | 'copyright', 'foundation', 'franklin', 'street', 'patent'
50 | }
51 | SIMILARITY_THRESHOLD = 0.75
52 |
53 | g = Github(os.environ['ORG_TOKEN'])
54 | repo = g.get_repo(os.environ['GITHUB_REPOSITORY'])
55 |
56 | # Load permitted licenses
57 | org_repo = repo.organization.get_repo(".github")
58 | licenses_file = org_repo.get_contents("permissive_licenses.json")
59 | permitted_licenses = json.loads(licenses_file.decoded_content)['permissive']
60 |
61 | # Get license file content
62 | license_text = ""
63 | try:
64 | for f in repo.get_contents(""):
65 | if f.name.lower().startswith(('license', 'copying')):
66 | license_text = f.decoded_content.decode().lower()
67 | break
68 | except GithubException:
69 | pass
70 |
71 | # Preprocess text
72 | if license_text:
73 | lines = license_text.split('\n')[:20]
74 | clean_text = ' '.join([
75 | word for line in lines
76 | for word in re.findall(r'\w+', line)
77 | if word not in EXCLUDED_KEYWORDS
78 | ])
79 |
80 | # Generate candidate phrases
81 | candidates = re.findall(r'\w+', clean_text)
82 | phrases = [' '.join(candidates[i:i+3]) for i in range(len(candidates)-2)]
83 | all_candidates = set(candidates + phrases)
84 |
85 | # Fuzzy match against permitted licenses
86 | is_permissive = False
87 | for license_name in permitted_licenses:
88 | license_lower = license_name.lower()
89 | for candidate in all_candidates:
90 | ratio = SequenceMatcher(None, candidate, license_lower).ratio()
91 | if ratio >= SIMILARITY_THRESHOLD:
92 | is_permissive = True
93 | break
94 | if is_permissive:
95 | break
96 | else:
97 | # No license file found
98 | is_permissive = False
99 |
100 | # Set output
101 | print(f"license_status={'permissive' if is_permissive else 'non-permissive'}")
102 | with open(os.environ['GITHUB_OUTPUT'], 'a') as fh:
103 | fh.write(f'license_status={"permissive" if is_permissive else "non-permissive"}')
104 |
105 |
106 |
--------------------------------------------------------------------------------
/permissive_licenses.json:
--------------------------------------------------------------------------------
1 | {
2 | "permissive": [
3 | "0BSD",
4 | "Adobe-2006 License",
5 | "AFL-1.1",
6 | "AFL-1.2",
7 | "AFL-2.1",
8 | "AFL-3.0",
9 | "Apache-1.0",
10 | "Apache-1.1",
11 | "Apache-2.0",
12 | "Artistic-1.0",
13 | "Artistic-1.0-cl8",
14 | "Artistic-1.0-Perl",
15 | "Artistic-2.0",
16 | "BlueOak-1.0.0",
17 | "Broadcom_Proprietary",
18 | "BSD-1-Clause",
19 | "BSD-2-Clause",
20 | "BSD-2-Clause-FreeBSD",
21 | "BSD-2-Clause-NetBSD",
22 | "BSD-2-Clause-Patent",
23 | "BSD-3-Clause",
24 | "BSD-3-Clause-Attribution",
25 | "BSD-3-Clause-Clear",
26 | "BSD-4-Clause",
27 | "BSD-4-Clause-UC",
28 | "BSL-1.0",
29 | "bzip2-1.0.5",
30 | "bzip2-1.0.6",
31 | "CC-BY-1.0",
32 | "CC-BY-2.0",
33 | "CC-BY-2.5",
34 | "CC-BY-3.0",
35 | "CC-BY-4.0",
36 | "CC0-1.0",
37 | "CNRI-Python",
38 | "curl License",
39 | "EDL-1.0",
40 | "FTL",
41 | "GFDL 1.2",
42 | "GFDL 1.3",
43 | "GFDL v1.1",
44 | "HPND",
45 | "ICU License",
46 | "ImageMagick License",
47 | "ISC License",
48 | "Jam STAPL Software License",
49 | "Lattice Diamond Programmer Embedded Software",
50 | "libpng License",
51 | "libpng-2.0",
52 | "MIT",
53 | "MIT License",
54 | "MIT-advertising License",
55 | "MIT-CMU License",
56 | "MIT-enna License",
57 | "MIT-feh License",
58 | "MITNFA",
59 | "Ms-PL",
60 | "NCSA",
61 | "NVIDIA_Proprietary",
62 | "OLDAP-2.8",
63 | "OpenSSL License",
64 | "PDDL-1.0",
65 | "PHP-3.01 License",
66 | "PostgreSQL License",
67 | "PSF-1.6a2",
68 | "PSF-1.6b1",
69 | "PSF-2.0",
70 | "PSF-2.1",
71 | "PSF-2.1.1",
72 | "PSF-2.3",
73 | "Public Domain",
74 | "Ruby License",
75 | "Sax Public Domain Notice",
76 | "SGI-B-1.0",
77 | "SGI-B-1.1",
78 | "TCL",
79 | "Unlicense",
80 | "UPL-1.0",
81 | "W3C",
82 | "VMW_Proprietary",
83 | "VMW_Proprietary_Component",
84 | "WTFPL",
85 | "X.Net",
86 | "X11",
87 | "zlib License",
88 | "zlib-acknowledgment License",
89 | "Expat",
90 | "Microsoft-Proprietary-SDK-License",
91 | "IJG",
92 | "ECL-2.0",
93 | "SSH-OpenSSH",
94 | "MIT-Modern-Variant",
95 | "libtiff",
96 | "X11",
97 | "BSD-3-Clause-acpica",
98 | "Unicode-DFS-2016",
99 | "NTP",
100 | "Cryptoki-Proprietary",
101 | "MinIO-Proprietary",
102 | "Highcharts-Proprietary",
103 | "Adobe-Glyph",
104 | "ADSL",
105 | "AML-glslang",
106 | "AML",
107 | "any-OSI",
108 | "Beerware",
109 | "Bison-exception-1.24",
110 | "Bison-exception-2.2",
111 | "Bitstream-Charter",
112 | "Bitstream-Vera",
113 | "blessing",
114 | "Boehm-GC",
115 | "Brian-Gladman-2-Clause",
116 | "BSD-2-Clause-Darwin",
117 | "BSD-2-Clause-first-lines",
118 | "BSD-2-Clause-Views",
119 | "BSD-3-Clause-flex",
120 | "BSD-3-Clause-LBNL",
121 | "BSD-3-Clause-Modification",
122 | "BSD-3-Clause-No-Military-License",
123 | "BSD-3-Clause-No-Nuclear-License",
124 | "BSD-3-Clause-No-Nuclear-Warranty",
125 | "BSD-3-Clause-Open-MPI",
126 | "BSD-3-Clause-Sun",
127 | "BSD-4-Clause-Shortened",
128 | "BSD-4.3RENO",
129 | "BSD-4.3TAHOE",
130 | "BSD-Attribution-HPND-disclaimer",
131 | "BSD-Source-beginning-file",
132 | "BSD-Source-Code",
133 | "BSD-Systemics",
134 | "Caldera-no-preamble",
135 | "CC-PDDC",
136 | "checkmk",
137 | "CMU-Mach-nodoc",
138 | "CMU-Mach",
139 | "Cronyx",
140 | "DOC",
141 | "DocBook-Schema",
142 | "dtoa",
143 | "EFL-1.0",
144 | "EFL-2.0",
145 | "etalab-2.0",
146 | "FreeBSD-DOC",
147 | "FSFAP-no-warranty-disclaimer",
148 | "FSFAP",
149 | "FSFUL",
150 | "FSFULLR",
151 | "FSFULLRWD",
152 | "Furuseth",
153 | "GCR-docs",
154 | "GD",
155 | "gtkbook",
156 | "HaskellReport",
157 | "hdparm",
158 | "HP-1986",
159 | "HP-1989",
160 | "HPND-doc-sell",
161 | "HPND-doc",
162 | "HPND-export-US-acknowledgement",
163 | "HPND-export-US-modify",
164 | "HPND-export-US",
165 | "HPND-export2-US",
166 | "HPND-Fenneberg-Livingston",
167 | "HPND-Kevlin-Henney",
168 | "HPND-Markus-Kuhn",
169 | "HPND-Pbmplus",
170 | "HPND-sell-variant-MIT-disclaimer",
171 | "HPND-sell-variant",
172 | "HPND-UC",
173 | "IBM-pibs",
174 | "Info-ZIP",
175 | "Inner-Net-2.0",
176 | "Intel",
177 | "ISC-Veillard",
178 | "Jam",
179 | "JPNIC",
180 | "JSON",
181 | "Kazlib",
182 | "libselinux-1.0",
183 | "LiLiQ-P-1.1",
184 | "Linux-man-pages-1-para",
185 | "Linux-OpenIB",
186 | "LLVM-exception",
187 | "LPD-document",
188 | "lsof",
189 | "Lucida-Bitmap-Fonts",
190 | "LZMA-SDK-9.22",
191 | "mailprio",
192 | "Martin-Birgmeier",
193 | "metamail",
194 | "MIT-0",
195 | "MIT-Festival",
196 | "MIT-Khronos-old",
197 | "MIT-open-group",
198 | "MIT-testregex",
199 | "MIT-Wu",
200 | "MPEG-SSG",
201 | "mplus",
202 | "Multics",
203 | "NAIST-2003",
204 | "NCGL-UK-2.0",
205 | "NCL",
206 | "NIST-PD-fallback",
207 | "NIST-PD",
208 | "NTP-0",
209 | "OFL-1.1-no-RFN",
210 | "OGC-1.0",
211 | "OGL-Canada-2.0",
212 | "OGL-UK-1.0",
213 | "OGL-UK-2.0",
214 | "OGL-UK-3.0",
215 | "OLDAP-2.0.1",
216 | "OML",
217 | "OpenSSL-standalone",
218 | "PADL",
219 | "PHP-3.0",
220 | "pkgconf",
221 | "radvd",
222 | "Rdisc",
223 | "RSA-MD",
224 | "SchemeReport",
225 | "Sendmail",
226 | "SGI-B-2.0",
227 | "SISSL",
228 | "SMLNJ",
229 | "snprintf",
230 | "softSurfer",
231 | "Spencer-86",
232 | "Spencer-94",
233 | "Spencer-99",
234 | "ssh-keyscan",
235 | "SSH-short",
236 | "SSLeay-standalone",
237 | "SunPro",
238 | "Swift-exception",
239 | "TCP-wrappers",
240 | "TermReadKey",
241 | "TMate",
242 | "TTWL",
243 | "TU-Berlin-1.0",
244 | "TU-Berlin-2.0",
245 | "Unicode-3.0",
246 | "Unicode-DFS-2015",
247 | "Unicode-TOU",
248 | "W3C-19980720",
249 | "W3C-20150513",
250 | "X11-distribute-modifications-variant",
251 | "Xfig",
252 | "XFree86-1.1",
253 | "xinetd",
254 | "xlock",
255 | "Zimbra-1.3",
256 | "Zimbra-1.4",
257 | "ZPL-2.1",
258 | "bcrypt-Solar-Designer",
259 | "MirOS",
260 | "DocBook-XML",
261 | "Genivia-Proprietary-License"
262 | ]
263 | }
264 |
--------------------------------------------------------------------------------
/profile/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Welcome!
4 |
5 | Collaboration, community and curiosity - all essential to a vibrant open source ethos and part of VMware’s culture. You’ll find us throughout the open source community, contributing to projects like Kubernetes, Linux, and TensorFlow. But we’re also hard at work solving technical challenges with innovative approaches and releasing those ideas as new open source projects. We’re proud of the creativity and contributions of VMware employees - from small but mighty open source projects to large, headline-grabbing community projects.
6 |
7 | # Finding your way
8 |
9 | If you’re looking for open source projects that serve our technologies such as VMware vSphere® or VMware NSX®, start in the [vmware](https://github.com/vmware) org; you’ll also find projects that address emerging technologies such as blockchain, machine learning, AI and data science. For smaller sample projects and code snippets browse our [vmware-labs](https://github.com/vmware-labs) org and the aptly named [vmware-samples](https://github.com/vmware-samples) org.
10 |
11 | Throughout these collections you’ll discover scripts, libraries, APIs, templates as well as complete solutions such as [Versatile Data Kit](https://github.com/vmware/versatile-data-kit) or [VMware Event Broker Appliance](https://github.com/vmware-samples/vcenter-event-broker-appliance).
12 |
13 | Browse the [vmware-tanzu](https://github.com/vmware-tanzu/) or the [vmware-tanzu-labs](https://github.com/vmware-tanzu-labs/) org to discover cloud native and modern applications related open source - think of Kubernetes and all its surrounding ecosystem. Recommended projects to explore include [Sonobuoy](https://github.com/vmware-tanzu/sonobuoy), [Carvel](https://github.com/vmware-tanzu/carvel), [Pinniped](https://github.com/vmware-tanzu/pinniped), and the newest member, [Cartographer](https://github.com/vmware-tanzu/cartographer).
14 |
15 | There’s even more VMware-backed open source to experience in [Clarity](https://github.com/vmware/clarity), [Spring](https://github.com/spring-projects), [RabbitMQ](https://github.com/rabbitmq), [Project Salt](https://github.com/saltstack/salt), and [Greenplum](https://github.com/greenplum-db).
16 |
17 | # Get Started
18 | Join our open source community: explore, experiment, ask questions, and contribute. Follow us [on Twitter](https://twitter.com/vmwopensource) and check in on the latest news and project updates [at our blog](https://blogs.vmware.com/opensource/).
19 |
20 |
21 | GPL Commitment
22 |
23 | Before filing or continuing to prosecute any legal proceeding or claim (other than a Defensive Action) arising from termination of a Covered License, VMware commits to extend to the person or entity ("you") accused of violating the Covered License the following provisions regarding cure and reinstatement, taken from GPL version 3. As used here, the term 'this License' refers to the specific Covered License being enforced.
24 |
25 | However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
26 |
27 | Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
28 |
29 | VMware intends this Commitment to be irrevocable, and binding and enforceable.
30 |
31 |
32 | ## Definitions
33 |
34 |
35 | 'Covered License' means the GNU General Public License, version 2 (GPLv2), the GNU Lesser General Public License, version 2.1 (LGPLv2.1), or the GNU Library General Public License, version 2 (LGPLv2), all as published by the Free Software Foundation.
36 |
37 | 'Defensive Action' means a legal proceeding or claim that VMware brings against you in response to a prior proceeding or claim initiated by you or your affiliate.
38 |
39 |
--------------------------------------------------------------------------------
/profile/image1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vmware/.github/82221468ce824ec9393dcae082e80ab5150f8556/profile/image1.jpg
--------------------------------------------------------------------------------
/reports/secret_report_20250217_002349.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | photon,5,Amazon AWS Access Key ID,open,https://github.com/vmware/photon/security/secret-scanning/5,2023-07-28T02:40:07Z,2024-09-12T07:52:46Z,
3 | photon,4,Amazon AWS Access Key ID,open,https://github.com/vmware/photon/security/secret-scanning/4,2023-07-28T02:40:07Z,2024-12-27T10:23:04Z,
4 | photon,3,Amazon AWS Secret Access Key,open,https://github.com/vmware/photon/security/secret-scanning/3,2023-07-28T02:40:07Z,2024-12-27T10:23:04Z,
5 | photon,2,Amazon AWS Access Key ID,open,https://github.com/vmware/photon/security/secret-scanning/2,2023-07-28T02:40:07Z,2024-12-27T10:23:04Z,
6 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
7 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
8 | vic,1,Slack Incoming Webhook URL,open,https://github.com/vmware/vic/security/secret-scanning/1,2023-10-02T02:22:38Z,2024-09-12T07:29:37Z,
9 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
10 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
11 | vic-ui,1,Slack Incoming Webhook URL,open,https://github.com/vmware/vic-ui/security/secret-scanning/1,2023-08-22T07:16:27Z,2024-09-12T07:29:37Z,
12 | mangle,1,Dynatrace API Token,open,https://github.com/vmware/mangle/security/secret-scanning/1,2023-10-03T11:20:20Z,2024-09-16T08:20:23Z,
13 | versatile-data-kit,5,Bitbucket Server Personal Access Token,open,https://github.com/vmware/versatile-data-kit/security/secret-scanning/5,2024-01-31T16:11:01Z,2024-01-31T16:11:01Z,
14 | versatile-data-kit,4,GitLab Access Token,open,https://github.com/vmware/versatile-data-kit/security/secret-scanning/4,2023-10-20T14:13:51Z,2023-10-20T14:13:51Z,
15 | versatile-data-kit,3,Slack Incoming Webhook URL,open,https://github.com/vmware/versatile-data-kit/security/secret-scanning/3,2023-06-28T14:08:44Z,2023-06-28T14:08:44Z,
16 | cloud-director-extension-standard-library,1,Slack Incoming Webhook URL,open,https://github.com/vmware/cloud-director-extension-standard-library/security/secret-scanning/1,2024-07-14T14:47:43Z,2024-09-11T22:06:23Z,
17 |
--------------------------------------------------------------------------------
/reports/secret_report_20250224_002324.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | photon,5,Amazon AWS Access Key ID,open,https://github.com/vmware/photon/security/secret-scanning/5,2023-07-28T02:40:07Z,2024-09-12T07:52:46Z,
3 | photon,4,Amazon AWS Access Key ID,open,https://github.com/vmware/photon/security/secret-scanning/4,2023-07-28T02:40:07Z,2024-12-27T10:23:04Z,
4 | photon,3,Amazon AWS Secret Access Key,open,https://github.com/vmware/photon/security/secret-scanning/3,2023-07-28T02:40:07Z,2024-12-27T10:23:04Z,
5 | photon,2,Amazon AWS Access Key ID,open,https://github.com/vmware/photon/security/secret-scanning/2,2023-07-28T02:40:07Z,2024-12-27T10:23:04Z,
6 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
7 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
8 | vic,1,Slack Incoming Webhook URL,open,https://github.com/vmware/vic/security/secret-scanning/1,2023-10-02T02:22:38Z,2024-09-12T07:29:37Z,
9 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
10 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
11 | vic-ui,1,Slack Incoming Webhook URL,open,https://github.com/vmware/vic-ui/security/secret-scanning/1,2023-08-22T07:16:27Z,2024-09-12T07:29:37Z,
12 | mangle,1,Dynatrace API Token,open,https://github.com/vmware/mangle/security/secret-scanning/1,2023-10-03T11:20:20Z,2024-09-16T08:20:23Z,
13 | versatile-data-kit,5,Bitbucket Server Personal Access Token,open,https://github.com/vmware/versatile-data-kit/security/secret-scanning/5,2024-01-31T16:11:01Z,2024-01-31T16:11:01Z,
14 | versatile-data-kit,4,GitLab Access Token,open,https://github.com/vmware/versatile-data-kit/security/secret-scanning/4,2023-10-20T14:13:51Z,2023-10-20T14:13:51Z,
15 | versatile-data-kit,3,Slack Incoming Webhook URL,open,https://github.com/vmware/versatile-data-kit/security/secret-scanning/3,2023-06-28T14:08:44Z,2023-06-28T14:08:44Z,
16 | cloud-director-extension-standard-library,1,Slack Incoming Webhook URL,open,https://github.com/vmware/cloud-director-extension-standard-library/security/secret-scanning/1,2024-07-14T14:47:43Z,2024-09-11T22:06:23Z,
17 |
--------------------------------------------------------------------------------
/reports/secret_report_20250303_002412.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
3 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
4 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
5 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
6 | mangle,1,Dynatrace API Token,open,https://github.com/vmware/mangle/security/secret-scanning/1,2023-10-03T11:20:20Z,2024-09-16T08:20:23Z,
7 |
--------------------------------------------------------------------------------
/reports/secret_report_20250310_002003.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
3 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
4 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
5 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
6 | mangle,1,Dynatrace API Token,open,https://github.com/vmware/mangle/security/secret-scanning/1,2023-10-03T11:20:20Z,2024-09-16T08:20:23Z,
7 |
--------------------------------------------------------------------------------
/reports/secret_report_20250317_002435.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
3 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
4 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
5 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
6 |
--------------------------------------------------------------------------------
/reports/secret_report_20250324_002442.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
3 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
4 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
5 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
6 |
--------------------------------------------------------------------------------
/reports/secret_report_20250331_002538.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
3 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
4 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
5 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
6 |
--------------------------------------------------------------------------------
/reports/secret_report_20250407_002459.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
3 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
4 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
5 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
6 |
--------------------------------------------------------------------------------
/reports/secret_report_20250414_002556.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
3 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
4 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
5 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
6 |
--------------------------------------------------------------------------------
/reports/secret_report_20250421_002953.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
3 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
4 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
5 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
6 |
--------------------------------------------------------------------------------
/reports/secret_report_20250428_002604.csv:
--------------------------------------------------------------------------------
1 | Repository,Alert ID,Secret Type,State,Alert URL,Created At,Updated At,Resolved Reason
2 | workflowTools,2,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/2,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
3 | workflowTools,1,Slack API Token,open,https://github.com/vmware/workflowTools/security/secret-scanning/1,2023-08-20T22:15:10Z,2024-12-27T10:15:13Z,
4 | alb-sdk,2,Amazon AWS Secret Access Key,open,https://github.com/vmware/alb-sdk/security/secret-scanning/2,2023-07-18T09:03:11Z,2024-12-27T10:45:58Z,
5 | alb-sdk,1,Amazon AWS Access Key ID,open,https://github.com/vmware/alb-sdk/security/secret-scanning/1,2023-07-18T09:03:10Z,2024-12-27T10:45:58Z,
6 |
--------------------------------------------------------------------------------
/scripts/check_licenses.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import re
4 | from github import Github, GithubException
5 |
6 | def check_license(repo):
7 | """Check if repository has a permissive license."""
8 | try:
9 | # Load permissive licenses from organization's .github repository
10 | org_github_repo = repo.organization.get_repo(".github")
11 | license_file = org_github_repo.get_contents(".github/permissive_licenses.json")
12 | permissive_licenses = json.loads(license_file.decoded_content.decode())['permissive']
13 | pattern = '|'.join(permissive_licenses)
14 |
15 | # Check for license file
16 | license_content = None
17 | try:
18 | contents = repo.get_contents("")
19 | for content in contents:
20 | if content.name.lower().startswith(('license', 'copying')):
21 | license_content = content.decoded_content.decode()
22 | break
23 | except GithubException as e:
24 | if e.status == 404:
25 | return False, "No license file found"
26 | raise
27 |
28 | if not license_content:
29 | return False, "No license file found"
30 |
31 | # Check first 20 lines for license match
32 | first_20_lines = '\n'.join(license_content.split('\n')[:20])
33 | if re.search(fr'\b({pattern})\b', first_20_lines, re.IGNORECASE):
34 | return True, "Permissive license found"
35 |
36 | return False, "Non-permissive license found"
37 |
38 | except Exception as e:
39 | return False, f"Error checking license: {str(e)}"
40 |
41 | def install_trigger(repo):
42 | """Install CLA trigger workflow from template."""
43 | try:
44 | # Get template from organization's .github repository
45 | org_github_repo = repo.organization.get_repo(".github")
46 | template = org_github_repo.get_contents(".github/templates/cla-trigger-template.yml")
47 | workflow_content = template.decoded_content.decode()
48 |
49 | # Ensure workflows directory exists
50 | try:
51 | repo.get_contents('.github/workflows')
52 | except GithubException as e:
53 | if e.status == 404:
54 | repo.create_file('.github/workflows/.gitkeep', 'Create workflows directory', '')
55 | else:
56 | raise
57 |
58 | # Create/update workflow
59 | workflow_path = '.github/workflows/cla-trigger.yml'
60 | try:
61 | existing_file = repo.get_contents(workflow_path)
62 | repo.update_file(workflow_path, 'Update CLA trigger', workflow_content, existing_file.sha)
63 | except GithubException as e:
64 | if e.status == 404:
65 | repo.create_file(workflow_path, 'Add CLA trigger', workflow_content)
66 | else:
67 | raise
68 |
69 | return True
70 | except Exception as e:
71 | print(f"Error installing trigger in {repo.full_name}: {str(e)}")
72 | return False
73 |
74 | def main():
75 | # Authenticate with bot's token
76 | token = os.environ.get('ORG_TOKEN')
77 | if not token:
78 | print("Error: ORG_TOKEN environment variable not set.")
79 | return 1
80 |
81 | g = Github(token)
82 | user = g.get_user()
83 |
84 | # Get included/excluded repos from environment variables
85 | excluded_repos = [r.strip() for r in os.environ.get('EXCLUDED_REPOS', '').split(',') if r.strip()]
86 | included_repos = [r.strip() for r in os.environ.get('INCLUDED_REPOS', '').split(',') if r.strip()]
87 |
88 | # Get repositories accessible to the bot via org membership
89 | repos = user.get_repos(affiliation="organization_member", visibility="all")
90 |
91 | results = {
92 | 'non_permissive': [],
93 | 'trigger_installed': [],
94 | 'excluded': excluded_repos,
95 | 'errors': []
96 | }
97 |
98 | for repo in repos:
99 | repo_full_name = repo.full_name
100 |
101 | # Apply filters
102 | if repo_full_name in excluded_repos:
103 | continue
104 | if included_repos and repo_full_name not in included_repos:
105 | continue
106 |
107 | print(f"Processing: {repo_full_name}")
108 |
109 | try:
110 | # License check
111 | is_permissive, msg = check_license(repo)
112 | if not is_permissive:
113 | results['non_permissive'].append({'repo': repo_full_name, 'reason': msg})
114 |
115 | # Install workflow
116 | if install_trigger(repo):
117 | results['trigger_installed'].append(repo_full_name)
118 | else:
119 | results['errors'].append(f"Failed to install trigger in {repo_full_name}")
120 |
121 | except Exception as e:
122 | results['errors'].append(f"{repo_full_name}: {str(e)}")
123 | continue
124 |
125 | # Save results
126 | with open('scan_results.json', 'w') as f:
127 | json.dump(results, f, indent=2)
128 |
129 | if results['non_permissive'] or results['errors']:
130 | print("Scan completed with findings:")
131 | print(json.dumps(results, indent=2))
132 | return 1
133 |
134 | print("Scan completed successfully - no issues found")
135 | return 0
136 |
137 | if __name__ == '__main__':
138 | exit(main())
139 |
140 |
--------------------------------------------------------------------------------
/scripts/dependency_scanner.py:
--------------------------------------------------------------------------------
1 | import os
2 | import csv
3 | import requests
4 | import logging
5 | import argparse
6 | from concurrent.futures import ThreadPoolExecutor, as_completed
7 | from datetime import datetime
8 | from requests.adapters import HTTPAdapter
9 | # Corrected import statement:
10 | from urllib3.util.retry import Retry # Import Retry directly from urllib3.util
11 | import sys
12 | import time
13 | import json
14 | import re
15 | import base64
16 | import xml.etree.ElementTree as ET
17 |
18 |
19 | class Logger:
20 | _instance = None
21 |
22 | def __new__(cls, log_level='INFO'):
23 | if cls._instance is None:
24 | cls._instance = super(Logger, cls).__new__(cls)
25 | numeric_level = getattr(logging, log_level.upper(), None)
26 | if not isinstance(numeric_level, int):
27 | raise ValueError(f'Invalid log level: {log_level}')
28 | logging.basicConfig(level=numeric_level, format='%(asctime)s - %(levelname)s - %(message)s')
29 | return cls._instance
30 |
31 |
32 | class GitHubClient:
33 | def __init__(self, token, max_retries=3, timeout=10): # Increased timeout
34 | self.token = token
35 | self.base_url = "https://api.github.com"
36 | self.headers = {
37 | "Authorization": f"Bearer {self.token}",
38 | "Accept": "application/vnd.github+json", # This is crucial for the SBOM API
39 | "X-GitHub-Api-Version": "2022-11-28",
40 | "User-Agent": "dependency-alerts-report-script"
41 | }
42 | self.max_retries = max_retries
43 | self.timeout = timeout # Use the timeout
44 | self.session = self._create_session()
45 | self.logger = Logger()
46 | self.rate_limit_remaining = None
47 | self.rate_limit_reset = None
48 |
49 |
50 | def _create_session(self):
51 | session = requests.Session()
52 | session.headers.update(self.headers)
53 | retry_strategy = Retry(
54 | total=self.max_retries,
55 | backoff_factor=2,
56 | status_forcelist=[429, 500, 502, 503, 504],
57 | allowed_methods=["GET"] # Only retry GET requests
58 | )
59 | adapter = HTTPAdapter(max_retries=retry_strategy)
60 | session.mount("https://", adapter)
61 | session.mount("http://", adapter)
62 | return session
63 |
64 | def _handle_rate_limit(self):
65 | if self.rate_limit_remaining is None:
66 | self.validate_token()
67 |
68 | if self.rate_limit_remaining < 50: # More conservative threshold
69 | wait_time = (self.rate_limit_reset - datetime.now()).total_seconds() + 5
70 | if wait_time > 0:
71 | logging.info(f"Rate limit approaching. Waiting for {wait_time:.0f} seconds.")
72 | time.sleep(wait_time)
73 | self.validate_token() # Re-validate after waiting
74 |
75 | def _request(self, method, url, **kwargs):
76 | self._handle_rate_limit()
77 | try:
78 | # Add timeout to the request
79 | response = self.session.request(method, url, timeout=self.timeout, **kwargs)
80 | response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
81 |
82 | if 'X-RateLimit-Remaining' in response.headers:
83 | self.rate_limit_remaining = int(response.headers['X-RateLimit-Remaining'])
84 | self.rate_limit_reset = datetime.fromtimestamp(int(response.headers['X-RateLimit-Reset']))
85 | return response
86 |
87 | except requests.exceptions.RequestException as e:
88 | logging.exception(f"Request failed: {e}")
89 | raise
90 | except requests.exceptions.Timeout: # Handle timeout specifically
91 | logging.error(f"Request to {url} timed out after {self.timeout} seconds.")
92 | raise
93 |
94 | def validate_token(self):
95 | """Validates the GitHub token and retrieves initial rate limit information."""
96 | url = f"{self.base_url}/rate_limit"
97 | try:
98 | response = self.session.get(url)
99 | response.raise_for_status()
100 | rate_limit = response.json()['resources']['core']
101 | self.rate_limit_remaining = rate_limit['remaining']
102 | self.rate_limit_reset = datetime.fromtimestamp(rate_limit['reset'])
103 |
104 | logging.info(f"Rate Limit: {self.rate_limit_remaining} remaining. Reset at {self.rate_limit_reset}")
105 | except requests.exceptions.RequestException as e:
106 | logging.exception(f"Token validation failed: {e}")
107 | raise
108 |
109 | def get_repositories(self, org_name, repo_list=None):
110 | """Retrieves a list of repositories to scan. Prioritizes org, then list."""
111 | repositories = []
112 | if org_name:
113 | # Fetch all repos in the organization (with pagination)
114 | url = f"{self.base_url}/orgs/{org_name}/repos?per_page=100"
115 | while url:
116 | response = self._request("GET", url)
117 | for repo in response.json():
118 | repositories.append({"name": repo["name"], "owner": repo["owner"]["login"]})
119 | url = response.links.get("next", {}).get("url")
120 |
121 | elif repo_list:
122 | # Use the provided comma-separated list
123 | for repo_name in repo_list.split(","):
124 | parts = repo_name.strip().split("/")
125 | if len(parts) == 2:
126 | owner, repo = parts
127 | else:
128 | owner = os.environ.get("GITHUB_REPOSITORY", "/").split("/")[0]
129 | repo = parts[0]
130 | repositories.append({"name": repo, "owner": owner})
131 | else:
132 | # Default to the current repository
133 | full_repo = os.environ.get("GITHUB_REPOSITORY")
134 | if not full_repo:
135 | raise ValueError("GITHUB_REPOSITORY environment variable is not set.")
136 | owner, repo = full_repo.split("/")
137 | repositories.append({"name": repo, "owner": owner})
138 |
139 | return repositories
140 |
141 | def get_dependabot_alerts(self, owner, repo_name):
142 | """Retrieves Dependabot alerts for a single repository (with pagination)."""
143 | alerts = []
144 | url = f"{self.base_url}/repos/{owner}/{repo_name}/dependabot/alerts?per_page=100&state=open"
145 | while url:
146 | response = self._request("GET", url)
147 | if response.status_code == 404:
148 | logging.info(f"Dependabot alerts not available or repo not found for {owner}/{repo_name}.")
149 | return []
150 | response.raise_for_status()
151 | alerts.extend(response.json())
152 | url = response.links.get("next", {}).get("url")
153 | return alerts
154 |
155 | def get_sbom_dependencies(self, owner, repo_name):
156 | """Retrieves the SBOM for a repository and extracts dependency information."""
157 | url = f"{self.base_url}/repos/{owner}/{repo_name}/dependency-graph/sbom"
158 | try:
159 | response = self._request("GET", url)
160 | response.raise_for_status()
161 | sbom_data = response.json()
162 | dependencies = {}
163 | # Extract dependency information from the SBOM
164 | for package in sbom_data.get("sbom", {}).get("packages", []):
165 | if "name" in package and "versionInfo" in package:
166 | dependencies[package["name"]] = package["versionInfo"]
167 | return dependencies
168 | except requests.exceptions.RequestException as e:
169 | logging.exception(f"Failed to get SBOM for {owner}/{repo_name}: {e}")
170 | return {} # Return empty dict in case of failure.
171 |
172 |
173 | class DependencyScanner:
174 | """
175 | Scans GitHub repositories for vulnerable dependencies using the Dependabot alerts API.
176 | """
177 |
178 | def __init__(self, github_token, org_name=None, repo_list=None, log_level='INFO', max_workers=10, max_retries=3):
179 | """
180 | Initializes the DependencyScanner.
181 | """
182 | self.github_token = github_token
183 | self.org_name = org_name
184 | self.repo_list = repo_list
185 | self.max_workers = max_workers
186 | self.client = GitHubClient(github_token, max_retries)
187 | self.logger = Logger(log_level) # Use the custom Logger class
188 | self.total_vulnerabilities = 0
189 | self.processed_repos = 0
190 |
191 | def generate_csv_report(self, filename=None):
192 | """Generates a CSV report of vulnerable dependencies."""
193 |
194 | if filename is None:
195 | timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
196 | filename = f"vulnerability_report_{timestamp}.csv"
197 |
198 | reports_dir = "reports"
199 | os.makedirs(reports_dir, exist_ok=True)
200 | filepath = os.path.join(reports_dir, filename)
201 |
202 | all_vulnerabilities = []
203 | repositories = self.client.get_repositories(self.org_name, self.repo_list)
204 | if not repositories:
205 | logging.warning("No repositories found to scan.")
206 | print("__NO_REPOS__")
207 | return
208 |
209 | with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
210 | future_to_repo = {
211 | executor.submit(self.client.get_dependabot_alerts, repo["owner"], repo["name"]): repo
212 | for repo in repositories
213 | }
214 |
215 | for future in as_completed(future_to_repo):
216 | repo = future_to_repo[future]
217 | try:
218 | alerts = future.result()
219 | self.processed_repos += 1
220 | logging.info(f"Processed {repo['owner']}/{repo['name']}: Found {len(alerts)} alerts.")
221 |
222 | # Get *all* current dependency versions from the SBOM *once* per repo
223 | current_versions = self.client.get_sbom_dependencies(repo['owner'], repo['name'])
224 |
225 | for alert in alerts:
226 | # print(json.dumps(alert, indent=2)) # Uncomment for debugging.
227 | try:
228 | dependency = alert.get("dependency", {})
229 | pkg = dependency.get("package", {})
230 | package_name = pkg.get("name", "N/A")
231 |
232 | # --- Use SBOM data for current version ---
233 | current_version = current_versions.get(package_name, "N/A")
234 | # --- End SBOM data ---
235 |
236 | security_advisory = alert.get("security_advisory", {})
237 | # --- Use security_vulnerability, not vulnerabilities array ---
238 | security_vulnerability = alert.get("security_vulnerability", {})
239 | vulnerable_range = security_vulnerability.get("vulnerable_version_range", "N/A")
240 | # --- End Use security_vulnerability ---
241 |
242 | severity = security_advisory.get("severity", "N/A")
243 | alert_url = alert.get("html_url", "N/A") # Get alert URL
244 | # Create Excel hyperlink formula
245 | severity_link = f'=HYPERLINK("{alert_url}", "{severity}")'
246 |
247 | first_patched = security_vulnerability.get("first_patched_version", {})
248 | update_available = first_patched.get("identifier", "N/A") if first_patched else "N/A"
249 |
250 | #print(f"DEBUG: Data before append: {repo['owner']}/{repo['name']}, {package_name}, {current_version}, {vulnerable_range}, {severity}, {update_available}")
251 |
252 | all_vulnerabilities.append({
253 | "Repository Name": f"{repo['owner']}/{repo['name']}",
254 | "Package Name": package_name,
255 | "Current Version": current_version,
256 | "Vulnerable Versions": vulnerable_range,
257 | "Severity": severity_link, # Use the hyperlink formula
258 | "Update Available": update_available
259 | })
260 | self.total_vulnerabilities += 1
261 | except KeyError as e:
262 | logging.warning(f"Missing key in alert data for repo {repo['owner']}/{repo['name']}: {e}. Skipping.")
263 | print(f"KeyError: {e}") #KEEP
264 | continue
265 | except Exception as e:
266 | logging.exception(f"Error processing alert data for repo {repo['owner']}/{repo['name']}: {e}. Skipping.")
267 | print(f"Other Exception: {e}") #KEEP
268 | continue
269 | except Exception as e:
270 | logging.exception(f"Error processing repo {repo['owner']}/{repo['name']}: {e}")
271 |
272 | if not all_vulnerabilities:
273 | logging.info("No vulnerabilities found.")
274 | return
275 |
276 | with open(filepath, "w", newline="", encoding="utf-8") as csvfile:
277 | fieldnames = [
278 | "Repository Name",
279 | "Package Name",
280 | "Current Version",
281 | "Vulnerable Versions",
282 | "Severity",
283 | "Update Available",
284 | ]
285 | writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
286 | writer.writeheader()
287 | writer.writerows(all_vulnerabilities)
288 | logging.info(f"CSV report generated: {filepath}")
289 |
290 | def run_scan(self, filename=None):
291 | """Runs the complete scan and report generation."""
292 | self.generate_csv_report(filename)
293 |
294 | def get_stats(self):
295 | return {"total": self.total_vulnerabilities, "processed_repos": self.processed_repos}
296 |
297 |
298 | def main():
299 | parser = argparse.ArgumentParser(description="GitHub Dependency Scanner")
300 | parser.add_argument("--token", required=True, help="GitHub token")
301 | parser.add_argument("--output", required=True, help="Output CSV file path")
302 | parser.add_argument("--org", help="GitHub organization name (optional)")
303 | parser.add_argument("--repo-list", help="Comma-separated list of repositories (optional)")
304 | parser.add_argument("--log-level", default="INFO", help="Logging level (default: INFO)")
305 | parser.add_argument("--max-workers", type=int, default=10, help="Maximum concurrent workers (default: 10)")
306 | parser.add_argument("--max-retries", type=int, default=3, help="Maximum retries for API requests (default: 3)")
307 |
308 | args = parser.parse_args()
309 |
310 | if not args.org and not args.repo_list and not os.environ.get("GITHUB_REPOSITORY"):
311 | print("Error: Must specify either --org, --repo-list, or run within a GitHub Actions context.", file=sys.stderr)
312 | sys.exit(1)
313 |
314 | scanner = DependencyScanner(
315 | github_token=args.token,
316 | org_name=args.org,
317 | repo_list=args.repo_list,
318 | log_level=args.log_level,
319 | max_workers=args.max_workers,
320 | max_retries=args.max_retries
321 | )
322 | scanner.run_scan(args.output)
323 | stats = scanner.get_stats()
324 | print(f"__STATS_START__total={stats['total']},processed_repos={stats['processed_repos']}__STATS_END__")
325 |
326 |
327 | if __name__ == "__main__":
328 | main()
329 |
330 |
--------------------------------------------------------------------------------
/scripts/github_secret_scanner.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import requests
3 | import logging
4 | import csv
5 | from concurrent.futures import ThreadPoolExecutor, as_completed
6 | from datetime import datetime
7 | import sys
8 | import time
9 | from requests.adapters import HTTPAdapter
10 | from requests.packages.urllib3.util.retry import Retry
11 |
12 |
13 | class Logger:
14 | _instance = None
15 |
16 | def __new__(cls, log_level='INFO'):
17 | if cls._instance is None:
18 | cls._instance = super(Logger, cls).__new__(cls)
19 | numeric_level = getattr(logging, log_level.upper(), None)
20 | if not isinstance(numeric_level, int):
21 | raise ValueError(f'Invalid log level: {log_level}')
22 | logging.basicConfig(level=numeric_level, format='%(asctime)s - %(levelname)s - %(message)s')
23 | return cls._instance
24 |
25 |
26 | class GitHubClient:
27 | def __init__(self, token, max_retries=3):
28 | self.token = token
29 | self.base_url = "https://api.github.com"
30 | self.headers = {"Authorization": f"token {self.token}", "Accept": "application/vnd.github.v3+json"}
31 | self.max_retries = max_retries
32 | self.session = self._create_session()
33 | self.logger = Logger()
34 | self.rate_limit_remaining = None
35 | self.rate_limit_reset = None
36 |
37 | def _create_session(self):
38 | session = requests.Session()
39 | session.headers.update(self.headers)
40 | retry_strategy = Retry(
41 | total=self.max_retries,
42 | backoff_factor=2,
43 | status_forcelist=[429, 500, 502, 503, 504],
44 | allowed_methods=["GET"]
45 | )
46 | adapter = HTTPAdapter(max_retries=retry_strategy)
47 | session.mount("https://", adapter)
48 | session.mount("http://", adapter)
49 | return session
50 |
51 | def _handle_rate_limit(self):
52 | if self.rate_limit_remaining is None:
53 | self.validate_token()
54 |
55 | if self.rate_limit_remaining < 10:
56 | wait_time = (self.rate_limit_reset - datetime.now()).total_seconds() + 5
57 | if wait_time > 0:
58 | logging.info(f"Rate limit approaching. Waiting for {wait_time:.0f} seconds.")
59 | time.sleep(wait_time)
60 | self.validate_token()
61 |
62 | def _request(self, method, url, **kwargs):
63 | self._handle_rate_limit()
64 | try:
65 | response = self.session.request(method, url, **kwargs)
66 | response.raise_for_status()
67 |
68 | if 'X-RateLimit-Remaining' in response.headers:
69 | self.rate_limit_remaining = int(response.headers['X-RateLimit-Remaining'])
70 | self.rate_limit_reset = datetime.fromtimestamp(int(response.headers['X-RateLimit-Reset']))
71 |
72 | return response
73 | except requests.exceptions.RequestException as e:
74 | logging.exception(f"Request failed: {e}")
75 | raise
76 |
77 | def validate_token(self):
78 | url = f"{self.base_url}/rate_limit"
79 | try:
80 | response = self.session.get(url)
81 | response.raise_for_status()
82 | rate_limit = response.json()['resources']['core']
83 | self.rate_limit_remaining = rate_limit['remaining']
84 | self.rate_limit_reset = datetime.fromtimestamp(rate_limit['reset'])
85 |
86 | logging.info(f"Rate Limit: {self.rate_limit_remaining} remaining. Reset at {self.rate_limit_reset}")
87 | except requests.exceptions.RequestException as e:
88 | logging.exception(f"Token validation failed: {e}")
89 | raise
90 |
91 | def fetch_repositories(self, org):
92 | repos = []
93 | url = f"{self.base_url}/orgs/{org}/repos?per_page=100"
94 | try:
95 | while url:
96 | response = self._request("GET", url)
97 | repos.extend(response.json())
98 | url = response.links.get('next', {}).get('url')
99 | except requests.exceptions.RequestException as e:
100 | logging.exception(f"Failed to fetch repositories for {org}: {e}")
101 | raise
102 | return repos
103 |
104 | def fetch_secret_alerts(self, org, repo, state="open"):
105 | alerts = []
106 | url = f"{self.base_url}/repos/{org}/{repo}/secret-scanning/alerts?per_page=100&state={state}"
107 | try:
108 | while url:
109 | response = self._request("GET", url)
110 | alerts.extend(response.json())
111 | url = response.links.get('next', {}).get('url')
112 | except requests.exceptions.RequestException as e:
113 | logging.exception(f"Failed to fetch {state} alerts for {repo}: {e}")
114 | raise
115 | return alerts
116 |
117 |
118 | class SecretScanner:
119 | def __init__(self, org, token, output_file, include_inactive=False, log_level='INFO', max_workers=10, max_retries=3):
120 | self.org = org
121 | self.token = token
122 | self.output_file = output_file # This is now relative to the .github repo
123 | self.include_inactive = include_inactive
124 | self.max_workers = max_workers
125 | self.client = GitHubClient(self.token, max_retries)
126 | self.logger = Logger(log_level)
127 | self.total_alerts = 0
128 | self.inactive_alerts = 0
129 | self.active_alerts = 0
130 |
131 | def generate_report(self):
132 | try:
133 | self.client.validate_token()
134 | repos = self.client.fetch_repositories(self.org)
135 |
136 | if not repos:
137 | logging.warning("No repositories found in the organization.")
138 | print("__NO_REPOS__")
139 | return
140 |
141 | with open(self.output_file, mode='w', newline='', encoding='utf-8') as file:
142 | writer = csv.writer(file)
143 | writer.writerow(["Repository", "Alert ID", "Secret Type", "State", "Alert URL", "Created At", "Updated At", "Resolved Reason"])
144 |
145 | with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
146 | future_to_repo = {}
147 | for repo in repos:
148 | future_to_repo[executor.submit(self.client.fetch_secret_alerts, self.org, repo['name'], "open")] = (repo, "open")
149 |
150 | if self.include_inactive:
151 | for repo in repos:
152 | future_to_repo[executor.submit(self.client.fetch_secret_alerts, self.org, repo['name'], "fixed")] = (repo, "fixed")
153 | for repo in repos:
154 | future_to_repo[executor.submit(self.client.fetch_secret_alerts, self.org, repo['name'], "resolved")] = (repo, "resolved")
155 |
156 |
157 | for future in as_completed(future_to_repo):
158 | (repo, state) = future_to_repo[future]
159 | try:
160 | alerts = future.result()
161 | logging.info(f"Processing {repo['name']} ({state} alerts): Found {len(alerts)} alerts.")
162 | for alert in alerts:
163 | self.total_alerts += 1
164 | if state == "open":
165 | self.active_alerts += 1
166 | else:
167 | self.inactive_alerts += 1
168 |
169 | resolved_reason = alert.get('resolution_comment') if state == 'resolved' else ''
170 |
171 | writer.writerow([
172 | repo['name'],
173 | alert['number'],
174 | alert.get('secret_type_display_name', alert.get('secret_type', 'Unknown')),
175 | alert['state'],
176 | alert['html_url'],
177 | alert['created_at'],
178 | alert['updated_at'],
179 | resolved_reason
180 | ])
181 | except Exception as e:
182 | logging.exception(f"Error processing alerts for {repo['name']}: {e}")
183 |
184 | logging.info(f"Report generated: {self.output_file}")
185 | logging.info(f"Total alerts found: {self.total_alerts}")
186 | logging.info(f"Active alerts: {self.active_alerts}")
187 | logging.info(f"Inactive alerts: {self.inactive_alerts}")
188 |
189 | except Exception as e:
190 | logging.exception(f"Failed to generate report: {e}")
191 | sys.exit(1)
192 |
193 | def get_stats(self):
194 | return {"total": self.total_alerts, "active": self.active_alerts, "inactive": self.inactive_alerts}
195 |
196 |
197 | def main():
198 | parser = argparse.ArgumentParser(description="GitHub Secret Scanner")
199 | parser.add_argument("--org", required=True, help="GitHub organization name")
200 | parser.add_argument("--token", required=True, help="GitHub token")
201 | parser.add_argument("--output", required=True, help="Output CSV file path") # Now relative path
202 | parser.add_argument("--include-inactive", action='store_true', help="Include inactive alerts in the report")
203 | parser.add_argument("--log-level", default="INFO", help="Logging level")
204 | parser.add_argument("--max-workers", type=int, default=10, help="Maximum concurrent workers")
205 | parser.add_argument("--max-retries", type=int, default=3, help="Maximum retries for API requests")
206 |
207 | args = parser.parse_args()
208 |
209 | try:
210 | scanner = SecretScanner(args.org, args.token, args.output, args.include_inactive, args.log_level, args.max_workers, args.max_retries)
211 | scanner.generate_report()
212 | stats = scanner.get_stats()
213 | print(f"__STATS_START__total={stats['total']},active={stats['active']},inactive={stats['inactive']}__STATS_END__")
214 |
215 | except Exception as e:
216 | logging.exception(f"An error occurred: {e}")
217 | sys.exit(1)
218 |
219 |
220 | if __name__ == "__main__":
221 | main()
222 |
223 |
--------------------------------------------------------------------------------
/scripts/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 |
--------------------------------------------------------------------------------
/signatures/CLA.json:
--------------------------------------------------------------------------------
1 | {
2 | "signedContributors": [
3 | {
4 | "name": "normansden",
5 | "id": 159940766,
6 | "comment_id": 2894313349,
7 | "created_at": "2025-05-20T12:58:48Z",
8 | "repoId": 921107922,
9 | "pullRequestNo": 14
10 | }
11 | ]
12 | }
--------------------------------------------------------------------------------
/templates/cla-trigger-template.yml:
--------------------------------------------------------------------------------
1 | name: CLA Check Trigger Template
2 |
3 | on:
4 | pull_request_target:
5 | types: [opened, synchronize, closed]
6 | issue_comment:
7 | types: [created]
8 | jobs:
9 | cla-check-trigger:
10 | uses: ${{ github.repository_owner }}/.github/.github/workflows/cla-workflow.yml@main
11 | secrets: inherit
12 |
--------------------------------------------------------------------------------