diff --git a/.gitignore b/.gitignore index 66ccb20..bceeefe 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,12 @@ test run_container.sh *.zip bin +# Note: Ignore any local dev scripts, but include ones used by the project scripts/*.py +!scripts/enrich_rules.py +!scripts/rewrite_messages.py +!scripts/update_cwe_catalog.py +!scripts/verify_jira_dashboard_config.py *.json markdown_overview_temp.md markdown_security_temp.md @@ -26,6 +31,9 @@ test.py file_generator.py .env *.md +!README.md +!docs/*.md +!tests/README.md test_results local_tests/ custom_rules/ @@ -103,4 +111,4 @@ logs/ .python-version .socket.fact.json -custom_rules/ \ No newline at end of file +custom_rules/ diff --git a/docs/parameters.md b/docs/parameters.md index f2312db..c30e785 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -432,6 +432,16 @@ socket-basics \ --jira-api-token "your-token" ``` +**Local Verification (No Jira API Calls)** +Use the helper script to confirm dashboard/env Jira settings are wired into the notifier: +```bash +./venv/bin/python scripts/verify_jira_dashboard_config.py +``` +Notes: +- The script only loads config and inspects notifier parameters; it does not contact Jira. +- It requires `SOCKET_SECURITY_API_KEY` (and usually `SOCKET_ORG`) to fetch dashboard config. +- You can use `INPUT_JIRA_*` env vars to simulate dashboard values. + ### GitHub Pull Request Comments **CLI Options:** diff --git a/scripts/verify_jira_dashboard_config.py b/scripts/verify_jira_dashboard_config.py new file mode 100644 index 0000000..f96a80a --- /dev/null +++ b/scripts/verify_jira_dashboard_config.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +""" +Verify that dashboard-provided Jira config (via Socket API) flows into JiraNotifier. + +This script does NOT call Jira. It only loads Socket Basics config, builds the +NotificationManager, and inspects JiraNotifier params. + +Expected env vars: +- SOCKET_SECURITY_API_KEY or SOCKET_SECURITY_API_TOKEN (required) +- SOCKET_ORG or SOCKET_ORG_SLUG (recommended; auto-discovery is attempted) +""" + +from __future__ import annotations + +import json +import logging +import os +import sys +from pathlib import Path + +from socket_basics.core.config import merge_json_and_env_config +from socket_basics.core.notification.manager import NotificationManager + + +def _summarize_jira(notifier) -> dict: + return { + "server": getattr(notifier, "server", None), + "project": getattr(notifier, "project", None), + "email": getattr(notifier, "email", None), + "api_token_present": bool(getattr(notifier, "api_token", None)), + } + + +def _load_dotenv(dotenv_path: Path) -> None: + if not dotenv_path.exists(): + return + try: + for raw_line in dotenv_path.read_text().splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if line.startswith("export "): + line = line[len("export "):].strip() + if "=" not in line: + continue + key, val = line.split("=", 1) + key = key.strip() + val = val.strip().strip("'").strip('"') + if key and key not in os.environ: + os.environ[key] = val + except Exception: + # Best-effort; do not fail if .env parsing is imperfect + pass + + +def main() -> int: + logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") + + repo_root = Path(__file__).resolve().parents[1] + _load_dotenv(repo_root / ".env") + + try: + config_dict = merge_json_and_env_config() + except Exception as exc: + print(f"ERROR: failed to load config: {exc}") + return 2 + + # Load notifications.yaml and build manager + try: + import yaml + + cfg_path = repo_root / "socket_basics" / "notifications.yaml" + notif_cfg = None + if cfg_path.exists(): + with open(cfg_path, "r") as f: + notif_cfg = yaml.safe_load(f) + except Exception as exc: + print(f"ERROR: failed to load notifications.yaml: {exc}") + return 2 + + nm = NotificationManager(notif_cfg, app_config=config_dict) + nm.load_from_config() + + jira_notifiers = [n for n in nm.notifiers if getattr(n, "name", "").lower() == "jira"] + if not jira_notifiers: + print("Jira notifier not enabled. Check that dashboard config includes jira_url or env provides JIRA_URL.") + return 1 + + # Print details for first Jira notifier + summary = _summarize_jira(jira_notifiers[0]) + print("Jira notifier config summary:") + print(json.dumps(summary, indent=2)) + + missing = [k for k, v in summary.items() if k in ("server", "project", "email") and not v] + if missing: + print(f"WARNING: missing expected fields: {', '.join(missing)}") + return 1 + + print("OK: Jira dashboard config appears to be wired into JiraNotifier.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/socket_basics/core/notification/jira_notifier.py b/socket_basics/core/notification/jira_notifier.py index 0fea3eb..fe0edd1 100644 --- a/socket_basics/core/notification/jira_notifier.py +++ b/socket_basics/core/notification/jira_notifier.py @@ -21,20 +21,23 @@ class JiraNotifier(BaseNotifier): def __init__(self, params: Dict[str, Any] | None = None): super().__init__(params or {}) # JIRA configuration from params, env variables, or app config + # Parameter names match dashboard config keys (jira_url, jira_project) self.server = ( - self.config.get('server') or + self.config.get('jira_url') or get_jira_url() ) self.project = ( - self.config.get('project') or + self.config.get('jira_project') or get_jira_project() ) self.email = ( - self.config.get('email') or + self.config.get('jira_email') or + self.config.get('auth', {}).get('email') or get_jira_email() ) self.api_token = ( - self.config.get('api_token') or + self.config.get('jira_api_token') or + self.config.get('auth', {}).get('api_token') or get_jira_api_token() ) diff --git a/socket_basics/core/notification/manager.py b/socket_basics/core/notification/manager.py index 095855c..e98464b 100644 --- a/socket_basics/core/notification/manager.py +++ b/socket_basics/core/notification/manager.py @@ -209,13 +209,13 @@ def load_from_config(self) -> None: env_var = p.get('env_variable') p_type = p.get('type', 'str') - # Resolve value: app_config -> env var -> default - val = None - if self.app_config and pname in self.app_config: - val = self.app_config.get(pname) - if env_var and os.getenv(env_var) is not None: + # Resolve value priority: app_config (highest) -> env var -> default (lowest) + val = p_default + + # Check env var (overrides default) + if env_var: ev = os.getenv(env_var) - if ev is not None: + if ev is not None and str(ev).strip() != "": if p_type == 'bool': val = ev.lower() == 'true' elif p_type == 'int': @@ -223,13 +223,14 @@ def load_from_config(self) -> None: val = int(ev) except Exception: logger.warning("Failed to convert notifier param %s=%s to int for notifier %s; using default %s", pname, ev, name, p_default) - val = p_default else: val = ev - else: - val = p_default - else: - val = p_default + + # Check app_config (highest priority, overrides env var) + if self.app_config and pname in self.app_config: + app_val = self.app_config.get(pname) + if app_val is not None: + val = app_val params[pname] = val else: diff --git a/socket_basics/notifications.yaml b/socket_basics/notifications.yaml index 066873e..20dce3f 100644 --- a/socket_basics/notifications.yaml +++ b/socket_basics/notifications.yaml @@ -61,11 +61,11 @@ notifiers: module_path: "socket_basics.core.notification.jira_notifier" class: "JiraNotifier" parameters: - - name: server + - name: jira_url option: --jira-url env_variable: INPUT_JIRA_URL type: str - - name: project + - name: jira_project option: --jira-project env_variable: INPUT_JIRA_PROJECT type: str diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..d252e75 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,12 @@ +# Tests + +Quick-start for running tests locally: + +```bash +./venv/bin/python -m pytest +``` + +Notes: +- Tests are lightweight and should not require external services. +- Use the local config-wiring script for Jira setup validation: + `./venv/bin/python scripts/verify_jira_dashboard_config.py` diff --git a/tests/test_jira_notifier_params.py b/tests/test_jira_notifier_params.py new file mode 100644 index 0000000..6bf3f4f --- /dev/null +++ b/tests/test_jira_notifier_params.py @@ -0,0 +1,44 @@ +from socket_basics.core.notification.jira_notifier import JiraNotifier +from socket_basics.core.config import normalize_api_config + + +def test_jira_notifier_reads_new_param_names(): + n = JiraNotifier( + { + "jira_url": "https://acme.atlassian.net", + "jira_project": "SEC", + "jira_email": "bot@acme.example", + "jira_api_token": "token123", + } + ) + assert n.server == "https://acme.atlassian.net" + assert n.project == "SEC" + assert n.email == "bot@acme.example" + assert n.api_token == "token123" + + +def test_jira_notifier_falls_back_to_auth_dict(): + n = JiraNotifier( + { + "jira_url": "https://acme.atlassian.net", + "jira_project": "SEC", + "auth": {"email": "auth@acme.example", "api_token": "auth-token"}, + } + ) + assert n.email == "auth@acme.example" + assert n.api_token == "auth-token" + + +def test_normalize_api_config_maps_jira_keys(): + normalized = normalize_api_config( + { + "jiraUrl": "https://acme.atlassian.net", + "jiraProject": "SEC", + "jiraEmail": "bot@acme.example", + "jiraApiToken": "token123", + } + ) + assert normalized["jira_url"] == "https://acme.atlassian.net" + assert normalized["jira_project"] == "SEC" + assert normalized["jira_email"] == "bot@acme.example" + assert normalized["jira_api_token"] == "token123" diff --git a/tests/test_notification_manager_params.py b/tests/test_notification_manager_params.py new file mode 100644 index 0000000..6e6dbe3 --- /dev/null +++ b/tests/test_notification_manager_params.py @@ -0,0 +1,69 @@ +import os + +from socket_basics.core.notification.manager import NotificationManager + + +def _base_cfg(): + return { + "notifiers": { + "jira": { + "module_path": "socket_basics.core.notification.jira_notifier", + "class": "JiraNotifier", + "parameters": [ + {"name": "jira_url", "env_variable": "INPUT_JIRA_URL", "type": "str"}, + {"name": "jira_project", "env_variable": "INPUT_JIRA_PROJECT", "type": "str"}, + {"name": "jira_email", "env_variable": "INPUT_JIRA_EMAIL", "type": "str"}, + {"name": "jira_api_token", "env_variable": "INPUT_JIRA_API_TOKEN", "type": "str"}, + ], + } + } + } + + +def test_param_precedence_app_config_over_env_and_default(monkeypatch): + cfg = _base_cfg() + # default should be overridden by env, then app_config should win + cfg["notifiers"]["jira"]["parameters"][0]["default"] = "https://default.example" + monkeypatch.setenv("INPUT_JIRA_URL", "https://env.example") + + nm = NotificationManager(cfg, app_config={"jira_url": "https://app.example"}) + nm.load_from_config() + + jira = next(n for n in nm.notifiers if getattr(n, "name", "") == "jira") + assert jira.server == "https://app.example" + + +def test_param_precedence_env_over_default(monkeypatch): + cfg = _base_cfg() + cfg["notifiers"]["jira"]["parameters"][0]["default"] = "https://default.example" + monkeypatch.setenv("INPUT_JIRA_URL", "https://env.example") + + nm = NotificationManager(cfg, app_config={}) + nm.load_from_config() + + jira = next(n for n in nm.notifiers if getattr(n, "name", "") == "jira") + assert jira.server == "https://env.example" + + +def test_param_precedence_default_used_when_no_env_or_app_config(monkeypatch): + cfg = _base_cfg() + # Use default for jira_project while enabling via app_config jira_url + cfg["notifiers"]["jira"]["parameters"][1]["default"] = "DEFAULTPROJ" + monkeypatch.delenv("INPUT_JIRA_PROJECT", raising=False) + + nm = NotificationManager(cfg, app_config={"jira_url": "https://app.example"}) + nm.load_from_config() + + jira = next(n for n in nm.notifiers if getattr(n, "name", "") == "jira") + assert jira.project == "DEFAULTPROJ" + + +def test_jira_enabled_via_app_config(monkeypatch): + cfg = _base_cfg() + monkeypatch.delenv("INPUT_JIRA_URL", raising=False) + + nm = NotificationManager(cfg, app_config={"jira_url": "https://app.example"}) + nm.load_from_config() + + jira = next(n for n in nm.notifiers if getattr(n, "name", "") == "jira") + assert jira.server == "https://app.example"