Created
April 29, 2026 07:13
-
-
Save jmingov/3bdfc1d0d4686466e2454961fce5625f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| GitHub Org Scanner | |
| ------------------ | |
| Scans all repos in one or more GitHub organizations and extracts signals about | |
| tech stack, databases, messaging, file transfer, integrations, security, | |
| infrastructure, and documentation coverage. | |
| Usage: | |
| pip install PyGithub pandas | |
| export GITHUB_TOKEN=... | |
| python github_org_scan.py --org org-one --org org-two | |
| python github_org_scan.py --orgs org-one,org-two | |
| Output: | |
| org_scan.csv — one row per repo, all signals | |
| org_scan_summary — printed to stdout | |
| """ | |
| import argparse | |
| import os | |
| import re | |
| import csv | |
| from datetime import datetime, timezone | |
| import time | |
| # ── config ──────────────────────────────────────────────────────────────────── | |
| DEFAULT_TOKEN = os.environ.get("GITHUB_TOKEN", "your-github-token") | |
| DEFAULT_ORGS = os.environ.get("GITHUB_ORGS") or os.environ.get("GITHUB_ORG", "their-org-name") | |
| DEFAULT_OUTPUT = os.environ.get("GITHUB_ORG_SCAN_OUTPUT", "org_scan.csv") | |
| DEFAULT_LIMIT = None | |
| # ── helpers ─────────────────────────────────────────────────────────────────── | |
| def parse_args(): | |
| parser = argparse.ArgumentParser(description="Scan GitHub organizations for technology and risk signals.") | |
| parser.add_argument("--token", default=DEFAULT_TOKEN, help="GitHub token. Defaults to GITHUB_TOKEN.") | |
| parser.add_argument( | |
| "--org", | |
| action="append", | |
| help="GitHub organization. Repeat for multiple orgs. Defaults to GITHUB_ORGS or GITHUB_ORG.", | |
| ) | |
| parser.add_argument( | |
| "--orgs", | |
| default=None, | |
| help="Comma-separated GitHub organizations. Defaults to GITHUB_ORGS or GITHUB_ORG.", | |
| ) | |
| parser.add_argument("--output", default=DEFAULT_OUTPUT, help="CSV output path.") | |
| parser.add_argument("--limit", type=int, default=DEFAULT_LIMIT, help="Scan only the first N repos per org.") | |
| return parser.parse_args() | |
| def selected_orgs(args): | |
| orgs = [] | |
| if args.org: | |
| orgs.extend(args.org) | |
| if args.orgs: | |
| orgs.extend(args.orgs.split(",")) | |
| if not orgs and DEFAULT_ORGS: | |
| orgs.extend(DEFAULT_ORGS.split(",")) | |
| return [org.strip() for org in orgs if org.strip()] | |
| def add_evidence(row, signal, source): | |
| if source: | |
| row["_evidence"].append(f"{signal}:{source}") | |
| def finalize_evidence(row): | |
| row["evidence"] = "; ".join(dict.fromkeys(row.pop("_evidence", []))) | |
| def first_matching_file(files, predicate): | |
| return next((f for f in files if predicate(f)), "") | |
| def safe_get(repo, path): | |
| try: | |
| return repo.get_contents(path).decoded_content.decode(errors="ignore") | |
| except Exception as e: | |
| print(f" warning: could not read {path}: {e}") | |
| return "" | |
| def get_file_tree(repo): | |
| return [f.path for f in repo.get_git_tree( | |
| repo.default_branch, recursive=True | |
| ).tree] | |
| def extract_hosts(text, pattern): | |
| return list(set(re.findall(pattern, text, re.IGNORECASE))) | |
| def check_libs(text, libs): | |
| return any(lib.lower() in text.lower() for lib in libs) | |
| def config_files(files): | |
| return [f for f in files if any(f.endswith(ext) for ext in [ | |
| "application.properties", | |
| "application.yml", | |
| "application.yaml", | |
| "bootstrap.properties", | |
| "bootstrap.yml", | |
| ])] | |
| def rate_limit_wait(g): | |
| rl = g.get_rate_limit().core | |
| if rl.remaining < 50: | |
| wait = max(0, int((rl.reset - datetime.now(timezone.utc)).total_seconds())) + 5 | |
| print(f" rate limit: waiting {wait}s...") | |
| time.sleep(wait) | |
| # ── fields ──────────────────────────────────────────────────────────────────── | |
| FIELDS = [ | |
| # identity | |
| "org", "repo", "archived", "last_commit", "last_commit_days_ago", | |
| "default_branch", "languages", "error", "evidence", | |
| # app type | |
| "has_pom", "is_spring_boot", "spring_boot_version", | |
| "is_spring_mvc", "is_j2ee", "is_dotnet", "is_nodejs", | |
| "is_python", "is_other", | |
| # databases | |
| "has_oracle", "has_mssql", "has_postgres", | |
| "has_mysql", "has_mariadb", | |
| "has_mongo", "has_redis", "has_elasticsearch", | |
| # messaging | |
| "has_kafka", "has_rabbitmq", "has_activemq", "has_jms", | |
| "kafka_topics", | |
| # file transfer | |
| "has_ftp", "has_sftp", "ftp_hosts", "sftp_hosts", | |
| # external integrations | |
| "has_soap", "has_rest_client", "has_graphql", | |
| "has_sap", "sap_hosts", "sap_clients", "sap_connection_type", | |
| "has_salesforce", "salesforce_instance", "salesforce_connection_type", | |
| "has_mulesoft", | |
| "external_urls", | |
| # security signals | |
| "has_jwt", "has_oauth2", "has_spring_security", "has_basic_auth", | |
| "has_ldap", "has_ssl_config", "has_vault", | |
| # session / exposure | |
| "has_cors_config", "has_csp", "has_secure_cookie", | |
| # crypto usage (what algorithms/libs are present) | |
| "has_md5", "has_sha1", "has_sha256", | |
| "has_bcrypt", "has_aes", "has_rsa", | |
| # infra / deployment | |
| "has_dockerfile", "has_k8s", "has_helm", "has_openshift", | |
| "has_terraform", "has_ansible", | |
| # docs signals | |
| "has_readme", "has_openapi", "has_asyncapi", | |
| "has_changelog", "has_docs_folder", | |
| # scheduled jobs | |
| "has_cron", "has_batch", "cron_expressions", | |
| # quality signals | |
| "has_tests", "has_sonar", "has_ci", | |
| ] | |
| # ── main scan ───────────────────────────────────────────────────────────────── | |
| args = parse_args() | |
| TOKEN = args.token | |
| ORG_NAMES = selected_orgs(args) | |
| OUTPUT = args.output | |
| LIMIT = args.limit | |
| if TOKEN == "your-github-token" or not ORG_NAMES or ORG_NAMES == ["their-org-name"]: | |
| raise SystemExit("Set GITHUB_TOKEN and GITHUB_ORGS, or pass --token and --org/--orgs.") | |
| from github import Github | |
| g = Github(TOKEN, per_page=100) | |
| results = [] | |
| for org_name in ORG_NAMES: | |
| try: | |
| org = g.get_organization(org_name) | |
| except Exception as e: | |
| print(f"warning: could not access org {org_name}: {e}") | |
| continue | |
| for i, repo in enumerate(org.get_repos()): | |
| if LIMIT and i >= LIMIT: | |
| break | |
| rate_limit_wait(g) | |
| print(f"scanning: {org_name}/{repo.name}") | |
| row = {f: False for f in FIELDS} | |
| row.update({ | |
| "org": org_name, | |
| "repo": repo.name, | |
| "archived": repo.archived, | |
| "last_commit": repo.pushed_at, | |
| "last_commit_days_ago": ( | |
| datetime.now(timezone.utc) - repo.pushed_at | |
| ).days if repo.pushed_at else None, | |
| "default_branch": repo.default_branch, | |
| "languages": ", ".join(repo.get_languages().keys()), | |
| "error": "", | |
| "evidence": "", | |
| "_evidence": [], | |
| "kafka_topics": "", | |
| "ftp_hosts": "", | |
| "sftp_hosts": "", | |
| "external_urls": "", | |
| "cron_expressions": "", | |
| "sap_hosts": "", | |
| "sap_clients": "", | |
| "sap_connection_type": "", | |
| "salesforce_instance": "", | |
| "salesforce_connection_type": "", | |
| "spring_boot_version": "", | |
| "has_cors_config": False, | |
| "has_csp": False, | |
| "has_secure_cookie": False, | |
| "has_md5": False, | |
| "has_sha1": False, | |
| "has_sha256": False, | |
| "has_bcrypt": False, | |
| "has_aes": False, | |
| "has_rsa": False, | |
| }) | |
| if repo.archived: | |
| finalize_evidence(row) | |
| results.append(row) | |
| continue | |
| try: | |
| files = get_file_tree(repo) | |
| # ── infra / deployment ──────────────────────────────────────────── | |
| row["has_dockerfile"] = any("Dockerfile" in f for f in files) | |
| row["has_k8s"] = any("k8s/" in f for f in files) | |
| row["has_helm"] = any("helm/" in f or "Chart.yaml" in f for f in files) | |
| row["has_openshift"] = any("openshift/" in f for f in files) | |
| row["has_terraform"] = any(f.endswith(".tf") for f in files) | |
| row["has_ansible"] = any("playbook" in f or "ansible" in f for f in files) | |
| add_evidence(row, "has_dockerfile", first_matching_file(files, lambda f: "Dockerfile" in f)) | |
| add_evidence(row, "has_k8s", first_matching_file(files, lambda f: "k8s/" in f)) | |
| add_evidence(row, "has_helm", first_matching_file(files, lambda f: "helm/" in f or "Chart.yaml" in f)) | |
| add_evidence(row, "has_openshift", first_matching_file(files, lambda f: "openshift/" in f)) | |
| add_evidence(row, "has_terraform", first_matching_file(files, lambda f: f.endswith(".tf"))) | |
| add_evidence(row, "has_ansible", first_matching_file(files, lambda f: "playbook" in f or "ansible" in f)) | |
| # ── docs signals ────────────────────────────────────────────────── | |
| row["has_readme"] = any(f.upper() == "README.MD" for f in files) | |
| row["has_openapi"] = any("openapi" in f.lower() or "swagger" in f.lower() for f in files) | |
| row["has_asyncapi"] = any("asyncapi" in f.lower() for f in files) | |
| row["has_changelog"] = any("CHANGELOG" in f.upper() for f in files) | |
| row["has_docs_folder"] = any(f.startswith("docs/") for f in files) | |
| add_evidence(row, "has_readme", first_matching_file(files, lambda f: f.upper() == "README.MD")) | |
| add_evidence(row, "has_openapi", first_matching_file(files, lambda f: "openapi" in f.lower() or "swagger" in f.lower())) | |
| add_evidence(row, "has_asyncapi", first_matching_file(files, lambda f: "asyncapi" in f.lower())) | |
| add_evidence(row, "has_changelog", first_matching_file(files, lambda f: "CHANGELOG" in f.upper())) | |
| add_evidence(row, "has_docs_folder", first_matching_file(files, lambda f: f.startswith("docs/"))) | |
| # ── language / framework detection ──────────────────────────────── | |
| row["is_dotnet"] = any(f.endswith(".csproj") or f.endswith(".sln") for f in files) | |
| row["is_nodejs"] = "package.json" in files | |
| row["is_python"] = any(f in files for f in [ | |
| "setup.py", "pyproject.toml", "requirements.txt" | |
| ]) | |
| add_evidence(row, "is_dotnet", first_matching_file(files, lambda f: f.endswith(".csproj") or f.endswith(".sln"))) | |
| add_evidence(row, "is_nodejs", first_matching_file(files, lambda f: f == "package.json")) | |
| add_evidence(row, "is_python", first_matching_file(files, lambda f: f in ["setup.py", "pyproject.toml", "requirements.txt"])) | |
| # ── quality signals ─────────────────────────────────────────────── | |
| row["has_tests"] = any("test" in f.lower() for f in files) | |
| row["has_sonar"] = any("sonar" in f.lower() for f in files) | |
| row["has_ci"] = any( | |
| f.startswith(".github/workflows") or | |
| "Jenkinsfile" in f or | |
| ".gitlab-ci" in f | |
| for f in files | |
| ) | |
| add_evidence(row, "has_tests", first_matching_file(files, lambda f: "test" in f.lower())) | |
| add_evidence(row, "has_sonar", first_matching_file(files, lambda f: "sonar" in f.lower())) | |
| add_evidence(row, "has_ci", first_matching_file(files, lambda f: ( | |
| f.startswith(".github/workflows") or "Jenkinsfile" in f or ".gitlab-ci" in f | |
| ))) | |
| # ── pom.xml analysis ────────────────────────────────────────────── | |
| pom_files = [f for f in files if f == "pom.xml" or f.endswith("/pom.xml")] | |
| if pom_files: | |
| pom = "\n".join(filter(None, (safe_get(repo, f) for f in pom_files))) | |
| row["has_pom"] = bool(pom) | |
| pom_source = ", ".join(pom_files[:5]) | |
| if len(pom_files) > 5: | |
| pom_source += f", +{len(pom_files) - 5} more" | |
| add_evidence(row, "has_pom", pom_source if pom else "") | |
| if pom: | |
| # app type | |
| row["is_spring_boot"] = "spring-boot-starter" in pom | |
| row["is_spring_mvc"] = "spring-webmvc" in pom and "spring-boot" not in pom | |
| row["is_j2ee"] = check_libs(pom, ["javax.ejb", "javaee", "jakarta.ejb"]) | |
| # spring boot version | |
| v = re.search(r"spring-boot[^<]*<version>([\d\.]+)", pom) | |
| if v: | |
| row["spring_boot_version"] = v.group(1) | |
| # databases | |
| row["has_oracle"] = check_libs(pom, ["ojdbc", "oracle-jdbc"]) | |
| row["has_mssql"] = check_libs(pom, ["mssql", "sqlserver"]) | |
| row["has_postgres"] = check_libs(pom, ["postgresql"]) | |
| row["has_mysql"] = check_libs(pom, ["mysql-connector", "mysql-connector-j"]) | |
| row["has_mariadb"] = check_libs(pom, ["mariadb-java-client", "mariadb"]) | |
| row["has_mongo"] = check_libs(pom, ["mongodb", "spring-data-mongodb"]) | |
| row["has_redis"] = check_libs(pom, ["redis", "lettuce", "jedis"]) | |
| row["has_elasticsearch"] = check_libs(pom, ["elasticsearch", "spring-data-elasticsearch"]) | |
| # messaging | |
| row["has_kafka"] = check_libs(pom, ["kafka"]) | |
| row["has_rabbitmq"] = check_libs(pom, ["rabbitmq", "amqp"]) | |
| row["has_activemq"] = check_libs(pom, ["activemq"]) | |
| row["has_jms"] = check_libs(pom, ["javax.jms", "jakarta.jms"]) | |
| # file transfer | |
| row["has_ftp"] = check_libs(pom, ["commons-net", "ftp4j", "edtftpj"]) | |
| row["has_sftp"] = check_libs(pom, ["jsch", "sshj", "apache-sshd", "ganymed"]) | |
| # integrations | |
| row["has_soap"] = check_libs(pom, ["jaxws", "cxf", "axis", "wsdl"]) | |
| row["has_rest_client"] = check_libs(pom, ["feign", "retrofit", "okhttp", "resttemplate"]) | |
| row["has_graphql"] = check_libs(pom, ["graphql"]) | |
| row["has_sap"] = check_libs(pom, ["sap", "sapjco", "sapidoc"]) | |
| row["has_salesforce"] = check_libs(pom, ["salesforce", "force.com"]) | |
| row["has_mulesoft"] = check_libs(pom, ["mule"]) | |
| # security | |
| row["has_jwt"] = check_libs(pom, ["jjwt", "nimbus-jose", "java-jwt"]) | |
| row["has_oauth2"] = check_libs(pom, ["oauth2", "spring-security-oauth", "spring-boot-starter-oauth2"]) | |
| row["has_spring_security"] = check_libs(pom, ["spring-security-core", "spring-boot-starter-security"]) | |
| row["has_ldap"] = check_libs(pom, ["ldap", "spring-ldap", "spring-security-ldap"]) | |
| row["has_basic_auth"] = check_libs(pom, ["spring-security-core", "spring-boot-starter-security"]) | |
| row["has_vault"] = check_libs(pom, ["vault"]) | |
| # crypto libraries in pom | |
| row["has_bcrypt"] = check_libs(pom, ["bcrypt", "jbcrypt"]) | |
| row["has_aes"] = check_libs(pom, ["bouncycastle", "bc-fips"]) | |
| row["has_rsa"] = check_libs(pom, ["bouncycastle", "bc-fips", "nimbus"]) | |
| # batch / scheduled | |
| row["has_batch"] = check_libs(pom, ["spring-batch"]) | |
| for signal in [ | |
| "is_spring_boot", "is_spring_mvc", "is_j2ee", | |
| "has_oracle", "has_mssql", "has_postgres", | |
| "has_mysql", "has_mariadb", "has_mongo", | |
| "has_redis", "has_elasticsearch", | |
| "has_kafka", "has_rabbitmq", "has_activemq", "has_jms", | |
| "has_ftp", "has_sftp", | |
| "has_soap", "has_rest_client", "has_graphql", | |
| "has_sap", "has_salesforce", "has_mulesoft", | |
| "has_jwt", "has_oauth2", "has_spring_security", | |
| "has_ldap", "has_basic_auth", "has_vault", | |
| "has_bcrypt", "has_aes", "has_rsa", "has_batch", | |
| ]: | |
| if row[signal]: | |
| add_evidence(row, signal, pom_source) | |
| # ── config file analysis ────────────────────────────────────────── | |
| cfg_files = config_files(files) | |
| all_config = "" | |
| for cf in cfg_files[:5]: # cap at 5 config files | |
| all_config += safe_get(repo, cf) | |
| cfg_source = ", ".join(cfg_files[:5]) | |
| if len(cfg_files) > 5: | |
| cfg_source += f", +{len(cfg_files) - 5} more" | |
| if all_config: | |
| # kafka topics | |
| topics = re.findall(r"(?:topic|topics)\s*[=:]\s*([\w\.\-]+)", all_config) | |
| row["kafka_topics"] = ", ".join(set(topics)) | |
| # ftp/sftp hosts | |
| row["ftp_hosts"] = ", ".join(extract_hosts( | |
| all_config, r"ftp://([a-zA-Z0-9\.\-]+)")) | |
| row["sftp_hosts"] = ", ".join(extract_hosts( | |
| all_config, r"sftp://([a-zA-Z0-9\.\-]+)")) | |
| # external URLs (filter out internal/localhost) | |
| urls = extract_hosts(all_config, r"https?://([a-zA-Z0-9\.\-]+\.[a-zA-Z]{2,})") | |
| external = [u for u in urls if not any( | |
| kw in u for kw in ["localhost", "internal", "svc.cluster", "127.0.0.1"] | |
| )] | |
| row["external_urls"] = ", ".join(set(external[:10])) # cap at 10 | |
| # cron expressions | |
| crons = re.findall(r"\d[\d\s\*\-\,\/]{8,}\d", all_config) | |
| row["cron_expressions"] = ", ".join(set(crons[:5])) | |
| row["has_cron"] = bool(crons) | |
| # ssl / vault | |
| row["has_ssl_config"] = "ssl" in all_config.lower() or "tls" in all_config.lower() | |
| row["has_vault"] = row["has_vault"] or "vault" in all_config.lower() | |
| # basic auth signals in config | |
| basic_auth_signals = [ | |
| "basic-auth", "basicauth", | |
| "security.user.password", # Spring Boot default basic auth | |
| "spring.security.user.password", # Spring Boot 2.x+ | |
| "Authorization: Basic", | |
| "httpBasic", | |
| ] | |
| row["has_basic_auth"] = row["has_basic_auth"] or check_libs(all_config, basic_auth_signals) | |
| # SAP connection details | |
| sap_hosts = re.findall(r"(?:ashost|mshost|gwhost|sap\.host|sap\.server)\s*[=:]\s*([\w\.\-]+)", all_config, re.IGNORECASE) | |
| sap_clients = re.findall(r"(?:sap\.client|jco\.client\.client|client)\s*[=:]\s*(\d{3})", all_config, re.IGNORECASE) | |
| sap_conn_types = [] | |
| if check_libs(all_config, ["jco.", "sapjco", "JCoDestination", "JCoFunction"]): | |
| sap_conn_types.append("JCo/RFC") | |
| if check_libs(all_config, ["idoc", "IDocDocument", "IDocFactory"]): | |
| sap_conn_types.append("IDoc") | |
| if check_libs(all_config, ["/sap/opu/odata", "OData"]): | |
| sap_conn_types.append("OData") | |
| if check_libs(all_config, ["bapi", "BAPI_"]): | |
| sap_conn_types.append("BAPI") | |
| row["sap_hosts"] = ", ".join(set(sap_hosts)) | |
| row["sap_clients"] = ", ".join(set(sap_clients)) | |
| row["sap_connection_type"] = ", ".join(sap_conn_types) | |
| row["has_sap"] = row["has_sap"] or bool(sap_hosts or sap_clients or sap_conn_types) | |
| # Salesforce connection details | |
| sf_instances = re.findall(r"([\w\-]+\.salesforce\.com|[\w\-]+\.force\.com)", all_config, re.IGNORECASE) | |
| sf_conn_types = [] | |
| if check_libs(all_config, ["RestTemplate", "HttpClient", "WebClient"]) and check_libs(all_config, ["salesforce", "force.com"]): | |
| sf_conn_types.append("REST") | |
| if check_libs(all_config, ["soap", "enterprise.wsdl", "partner.wsdl"]): | |
| sf_conn_types.append("SOAP") | |
| if check_libs(all_config, ["Platform Event", "PushTopic", "CometD", "Streaming"]): | |
| sf_conn_types.append("Streaming/Events") | |
| if check_libs(all_config, ["Bulk API", "bulk/v"]): | |
| sf_conn_types.append("Bulk API") | |
| if check_libs(all_config, ["connected app", "client_id", "client_secret"]) and check_libs(all_config, ["salesforce", "force.com"]): | |
| sf_conn_types.append("OAuth/Connected App") | |
| row["salesforce_instance"] = ", ".join(set(sf_instances)) | |
| row["salesforce_connection_type"] = ", ".join(sf_conn_types) | |
| row["has_salesforce"] = row["has_salesforce"] or bool(sf_instances or sf_conn_types) | |
| # session / exposure | |
| row["has_secure_cookie"] = check_libs(all_config, [ | |
| "cookie.secure=true", "setSecure(true)", | |
| "server.servlet.session.cookie.secure=true", | |
| ]) | |
| row["has_cors_config"] = check_libs(all_config, [ | |
| "cors", "CrossOrigin", "allowedOrigins", | |
| "Access-Control-Allow-Origin", | |
| ]) | |
| row["has_csp"] = check_libs(all_config, [ | |
| "Content-Security-Policy", "contentSecurityPolicy", | |
| ]) | |
| # crypto usage in config/code | |
| row["has_md5"] = check_libs(all_config, [ | |
| "MD5", "MessageDigest.getInstance(\"MD5\")", | |
| "DigestUtils.md5", | |
| ]) | |
| row["has_sha1"] = check_libs(all_config, [ | |
| "SHA-1", "SHA1", "MessageDigest.getInstance(\"SHA-1\")", | |
| "DigestUtils.sha1", | |
| ]) | |
| row["has_sha256"] = check_libs(all_config, [ | |
| "SHA-256", "SHA256", "MessageDigest.getInstance(\"SHA-256\")", | |
| "DigestUtils.sha256", | |
| ]) | |
| row["has_bcrypt"] = row["has_bcrypt"] or check_libs(all_config, [ | |
| "BCrypt", "bcrypt", "BCryptPasswordEncoder", | |
| ]) | |
| row["has_aes"] = row["has_aes"] or check_libs(all_config, [ | |
| "AES", "AES/CBC", "AES/GCM", "Cipher.getInstance(\"AES", | |
| ]) | |
| row["has_rsa"] = row["has_rsa"] or check_libs(all_config, [ | |
| "RSA", "Cipher.getInstance(\"RSA", "KeyPairGenerator", | |
| ]) | |
| for signal in [ | |
| "kafka_topics", "ftp_hosts", "sftp_hosts", "external_urls", | |
| "cron_expressions", "has_cron", "has_ssl_config", "has_vault", | |
| "has_basic_auth", "sap_hosts", "sap_clients", | |
| "sap_connection_type", "has_sap", "salesforce_instance", | |
| "salesforce_connection_type", "has_salesforce", | |
| "has_secure_cookie", "has_cors_config", "has_csp", | |
| "has_md5", "has_sha1", "has_sha256", | |
| "has_bcrypt", "has_aes", "has_rsa", | |
| ]: | |
| if row[signal]: | |
| add_evidence(row, signal, cfg_source) | |
| except Exception as e: | |
| row["error"] = str(e) | |
| finalize_evidence(row) | |
| results.append(row) | |
| # ── write CSV ───────────────────────────────────────────────────────────────── | |
| with open(OUTPUT, "w", newline="") as f: | |
| writer = csv.DictWriter(f, fieldnames=FIELDS) | |
| writer.writeheader() | |
| writer.writerows(results) | |
| print(f"\n✓ saved to {OUTPUT}\n") | |
| # ── summary ─────────────────────────────────────────────────────────────────── | |
| import pandas as pd | |
| df = pd.read_csv(OUTPUT) | |
| active = df[df.archived != True] | |
| stale = active[active.last_commit_days_ago > 365] | |
| by_org = df.groupby("org").size().to_dict() | |
| by_org_summary = "\n".join(f" {org}: {count:>6}" for org, count in by_org.items()) | |
| print(f""" | |
| ═══════════════════════════════════════ | |
| ORG SCAN SUMMARY — {", ".join(ORG_NAMES)} | |
| ═══════════════════════════════════════ | |
| Total repos: {len(df):>6} | |
| Active (not archived): {len(active):>6} | |
| Stale (>1yr no commit): {len(stale):>6} | |
| REPOS BY ORG | |
| {by_org_summary} | |
| APP TYPE | |
| Spring Boot: {active.is_spring_boot.sum():>6} | |
| Spring MVC: {active.is_spring_mvc.sum():>6} | |
| J2EE: {active.is_j2ee.sum():>6} | |
| .NET: {active.is_dotnet.sum():>6} | |
| Node.js: {active.is_nodejs.sum():>6} | |
| Python: {active.is_python.sum():>6} | |
| DATABASES | |
| Oracle: {active.has_oracle.sum():>6} | |
| MSSQL: {active.has_mssql.sum():>6} | |
| PostgreSQL: {active.has_postgres.sum():>6} | |
| MySQL: {active.has_mysql.sum():>6} | |
| MariaDB: {active.has_mariadb.sum():>6} | |
| MongoDB: {active.has_mongo.sum():>6} | |
| Redis: {active.has_redis.sum():>6} | |
| Elasticsearch: {active.has_elasticsearch.sum():>6} | |
| MESSAGING | |
| Kafka: {active.has_kafka.sum():>6} | |
| RabbitMQ: {active.has_rabbitmq.sum():>6} | |
| ActiveMQ: {active.has_activemq.sum():>6} | |
| JMS: {active.has_jms.sum():>6} | |
| FILE TRANSFER | |
| FTP: {active.has_ftp.sum():>6} | |
| SFTP: {active.has_sftp.sum():>6} | |
| INTEGRATIONS | |
| SAP: {active.has_sap.sum():>6} | |
| JCo/RFC: {active[active.sap_connection_type.str.contains("JCo", na=False)].shape[0]:>6} | |
| IDoc: {active[active.sap_connection_type.str.contains("IDoc", na=False)].shape[0]:>6} | |
| OData: {active[active.sap_connection_type.str.contains("OData", na=False)].shape[0]:>6} | |
| BAPI: {active[active.sap_connection_type.str.contains("BAPI", na=False)].shape[0]:>6} | |
| Salesforce: {active.has_salesforce.sum():>6} | |
| REST: {active[active.salesforce_connection_type.str.contains("REST", na=False)].shape[0]:>6} | |
| SOAP: {active[active.salesforce_connection_type.str.contains("SOAP", na=False)].shape[0]:>6} | |
| Streaming/Events: {active[active.salesforce_connection_type.str.contains("Streaming", na=False)].shape[0]:>6} | |
| Bulk API: {active[active.salesforce_connection_type.str.contains("Bulk", na=False)].shape[0]:>6} | |
| SOAP/WSDL: {active.has_soap.sum():>6} | |
| GraphQL: {active.has_graphql.sum():>6} | |
| MuleSoft: {active.has_mulesoft.sum():>6} | |
| SECURITY | |
| JWT: {active.has_jwt.sum():>6} | |
| OAuth2: {active.has_oauth2.sum():>6} | |
| Spring Security: {active.has_spring_security.sum():>6} | |
| Basic Auth: {active.has_basic_auth.sum():>6} | |
| LDAP: {active.has_ldap.sum():>6} | |
| Vault: {active.has_vault.sum():>6} | |
| SESSION / EXPOSURE | |
| Secure cookie: {active.has_secure_cookie.sum():>6} | |
| CORS configured: {active.has_cors_config.sum():>6} | |
| CSP configured: {active.has_csp.sum():>6} | |
| CRYPTO USAGE | |
| MD5: {active.has_md5.sum():>6} | |
| SHA-1: {active.has_sha1.sum():>6} | |
| SHA-256: {active.has_sha256.sum():>6} | |
| BCrypt: {active.has_bcrypt.sum():>6} | |
| AES: {active.has_aes.sum():>6} | |
| RSA: {active.has_rsa.sum():>6} | |
| INFRA | |
| Dockerfile: {active.has_dockerfile.sum():>6} | |
| Kubernetes/k8s: {active.has_k8s.sum():>6} | |
| Helm: {active.has_helm.sum():>6} | |
| OpenShift: {active.has_openshift.sum():>6} | |
| Terraform: {active.has_terraform.sum():>6} | |
| Ansible: {active.has_ansible.sum():>6} | |
| DOCS COVERAGE | |
| Has README: {active.has_readme.sum():>6} | |
| Has OpenAPI: {active.has_openapi.sum():>6} | |
| Has AsyncAPI: {active.has_asyncapi.sum():>6} | |
| Has Changelog: {active.has_changelog.sum():>6} | |
| Has CI: {active.has_ci.sum():>6} | |
| Has Tests: {active.has_tests.sum():>6} | |
| ═══════════════════════════════════════ | |
| """) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment