Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,6 +31,9 @@ test.py
file_generator.py
.env
*.md
!README.md
!docs/*.md
!tests/README.md
test_results
local_tests/
custom_rules/
Expand Down Expand Up @@ -103,4 +111,4 @@ logs/
.python-version
.socket.fact.json

custom_rules/
custom_rules/
10 changes: 10 additions & 0 deletions docs/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
104 changes: 104 additions & 0 deletions scripts/verify_jira_dashboard_config.py
Original file line number Diff line number Diff line change
@@ -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())
11 changes: 7 additions & 4 deletions socket_basics/core/notification/jira_notifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment on lines 25 to 31
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the params are being renamed, do you know if any customers use custom configs that may rely on the old JIRA keys (server or project)? If so, we may need to include some backwards compatibility fallback mechanism.

)
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()
Comment on lines 33 to 41
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same note here regarding the legacy keys (email and api_token in this case) - that said, if we don't think any customers are relying on these, it may be fine to ship as-is.

If it's worth including for maximum compatibility, we could do something like:

  self.config.get('jira_email') or
  self.config.get('email') or
  self.config.get('auth', {}).get('email') or
  get_jira_email()

And repeat the same pattern for api_token as well as project and server above.

)

Expand Down
23 changes: 12 additions & 11 deletions socket_basics/core/notification/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,27 +209,28 @@ 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':
try:
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:
Expand Down
4 changes: 2 additions & 2 deletions socket_basics/notifications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -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`
44 changes: 44 additions & 0 deletions tests/test_jira_notifier_params.py
Original file line number Diff line number Diff line change
@@ -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"
69 changes: 69 additions & 0 deletions tests/test_notification_manager_params.py
Original file line number Diff line number Diff line change
@@ -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"