gozu 0.3.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gozu-0.3.7/.dockerignore +12 -0
- gozu-0.3.7/.env.example +58 -0
- gozu-0.3.7/.github/workflows/ci.yml +32 -0
- gozu-0.3.7/.github/workflows/release.yml +69 -0
- gozu-0.3.7/.github/workflows/sonar-to-jira-main.yml +69 -0
- gozu-0.3.7/.gitignore +17 -0
- gozu-0.3.7/CLAUDE.md +187 -0
- gozu-0.3.7/Dockerfile +37 -0
- gozu-0.3.7/PKG-INFO +490 -0
- gozu-0.3.7/README.md +470 -0
- gozu-0.3.7/alembic.ini +160 -0
- gozu-0.3.7/docker-compose.yml +140 -0
- gozu-0.3.7/pyproject.toml +62 -0
- gozu-0.3.7/pytest.ini +5 -0
- gozu-0.3.7/sonar-project.properties +9 -0
- gozu-0.3.7/src/cli/__init__.py +3 -0
- gozu-0.3.7/src/cli/config_cmd/__init__.py +92 -0
- gozu-0.3.7/src/cli/config_cmd/edit.py +223 -0
- gozu-0.3.7/src/cli/config_lookup.py +87 -0
- gozu-0.3.7/src/cli/crash_handler.py +62 -0
- gozu-0.3.7/src/cli/help_links.py +38 -0
- gozu-0.3.7/src/cli/init_wizard/__init__.py +359 -0
- gozu-0.3.7/src/cli/init_wizard/env_step.py +85 -0
- gozu-0.3.7/src/cli/init_wizard/jira_step.py +87 -0
- gozu-0.3.7/src/cli/init_wizard/llm_step.py +20 -0
- gozu-0.3.7/src/cli/init_wizard/semgrep_step.py +21 -0
- gozu-0.3.7/src/cli/init_wizard/sonar_cloud.py +78 -0
- gozu-0.3.7/src/cli/init_wizard/sonar_local.py +118 -0
- gozu-0.3.7/src/cli/init_wizard/summary.py +50 -0
- gozu-0.3.7/src/cli/main.py +291 -0
- gozu-0.3.7/src/cli/prerequisites/__init__.py +22 -0
- gozu-0.3.7/src/cli/prerequisites/archive.py +106 -0
- gozu-0.3.7/src/cli/prerequisites/java.py +116 -0
- gozu-0.3.7/src/cli/prerequisites/semgrep.py +61 -0
- gozu-0.3.7/src/cli/prerequisites/sonar_scanner.py +108 -0
- gozu-0.3.7/src/cli/prerequisites/trivy.py +136 -0
- gozu-0.3.7/src/cli/prompts.py +49 -0
- gozu-0.3.7/src/cli/report.py +78 -0
- gozu-0.3.7/src/cli/scan_runner/__init__.py +197 -0
- gozu-0.3.7/src/cli/scan_runner/config_fields.py +28 -0
- gozu-0.3.7/src/cli/scan_runner/config_select.py +64 -0
- gozu-0.3.7/src/cli/scan_runner/scanner_exec.py +147 -0
- gozu-0.3.7/src/cli/scan_runner/semgrep_exec.py +24 -0
- gozu-0.3.7/src/cli/scan_runner/workflow_trigger.py +129 -0
- gozu-0.3.7/src/cli/stack/__init__.py +187 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/Dockerfile +1 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/__init__.py +0 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/alembic.ini +1 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/init_sonarqube_db.sql +29 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/README +1 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/env.py +85 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/script.py.mako +28 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/versions/0001_initial_schema.py +71 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/versions/0002_add_project_key.py +32 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/versions/0003_add_sonar_plan_and_branches.py +35 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/versions/0004_add_ticket_claims.py +55 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/versions/0005_add_ticket_destinations.py +67 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/versions/0006_add_ticket_claims_status.py +34 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/versions/0007_add_last_scan_tracking.py +38 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/db/migrations/versions/0008_add_ticket_cap.py +32 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/docker-compose.yml +1 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/pyproject.toml +1 -0
- gozu-0.3.7/src/cli/stack/_stackfiles/uv.lock +1 -0
- gozu-0.3.7/src/cli/stack/backup.py +77 -0
- gozu-0.3.7/src/cli/stack/cleanup.py +123 -0
- gozu-0.3.7/src/cli/stack/files.py +104 -0
- gozu-0.3.7/src/cli/stack/profiles.py +140 -0
- gozu-0.3.7/src/cli/stack/status_report.py +59 -0
- gozu-0.3.7/src/cli/stack/webhooks.py +53 -0
- gozu-0.3.7/src/cli/stack/wipe.py +117 -0
- gozu-0.3.7/src/cli/status.py +44 -0
- gozu-0.3.7/src/cli/wizard_engine.py +99 -0
- gozu-0.3.7/src/config/__init__.py +3 -0
- gozu-0.3.7/src/config/connection.py +41 -0
- gozu-0.3.7/src/config/crypto.py +60 -0
- gozu-0.3.7/src/config/migrations.py +100 -0
- gozu-0.3.7/src/config/store.py +221 -0
- gozu-0.3.7/src/config/ticket_destinations.py +122 -0
- gozu-0.3.7/src/core/__init__.py +3 -0
- gozu-0.3.7/src/core/constants.py +11 -0
- gozu-0.3.7/src/core/errors.py +55 -0
- gozu-0.3.7/src/core/models.py +94 -0
- gozu-0.3.7/src/dev/seed_test_config.py +97 -0
- gozu-0.3.7/src/github_action/__init__.py +3 -0
- gozu-0.3.7/src/github_action/main.py +180 -0
- gozu-0.3.7/src/llm/__init__.py +2 -0
- gozu-0.3.7/src/llm/enrich.py +147 -0
- gozu-0.3.7/src/llm/factory.py +19 -0
- gozu-0.3.7/src/receiver/__init__.py +3 -0
- gozu-0.3.7/src/receiver/app.py +141 -0
- gozu-0.3.7/src/receiver/starter.py +49 -0
- gozu-0.3.7/src/receiver/verify_signature.py +19 -0
- gozu-0.3.7/src/scanner/__init__.py +3 -0
- gozu-0.3.7/src/scanner/base.py +116 -0
- gozu-0.3.7/src/scanner/factory.py +78 -0
- gozu-0.3.7/src/scanner/html_text.py +63 -0
- gozu-0.3.7/src/scanner/screenshot.py +124 -0
- gozu-0.3.7/src/scanner/semgrep_local.py +186 -0
- gozu-0.3.7/src/scanner/sonarqube_classify.py +50 -0
- gozu-0.3.7/src/scanner/sonarqube_cloud.py +98 -0
- gozu-0.3.7/src/scanner/sonarqube_common.py +187 -0
- gozu-0.3.7/src/scanner/sonarqube_local.py +29 -0
- gozu-0.3.7/src/scanner/trivy_client.py +173 -0
- gozu-0.3.7/src/scripts/__init__.py +3 -0
- gozu-0.3.7/src/scripts/bootstrap_env.py +150 -0
- gozu-0.3.7/src/scripts/env_ports.py +70 -0
- gozu-0.3.7/src/scripts/fernet_safety.py +72 -0
- gozu-0.3.7/src/scripts/paths.py +29 -0
- gozu-0.3.7/src/temporal/__init__.py +3 -0
- gozu-0.3.7/src/temporal/activities/__init__.py +10 -0
- gozu-0.3.7/src/temporal/activities/capture_and_attach_screenshot.py +67 -0
- gozu-0.3.7/src/temporal/activities/create_tickets.py +148 -0
- gozu-0.3.7/src/temporal/activities/fetch_findings.py +74 -0
- gozu-0.3.7/src/temporal/activities/reconcile_resolved_findings.py +139 -0
- gozu-0.3.7/src/temporal/data_converter.py +11 -0
- gozu-0.3.7/src/temporal/models/__init__.py +6 -0
- gozu-0.3.7/src/temporal/models/create_tickets.py +20 -0
- gozu-0.3.7/src/temporal/models/fetch_findings.py +30 -0
- gozu-0.3.7/src/temporal/models/reconcile_resolved_findings.py +19 -0
- gozu-0.3.7/src/temporal/models/screenshot_attach.py +20 -0
- gozu-0.3.7/src/temporal/models/sonar_to_jira.py +51 -0
- gozu-0.3.7/src/temporal/worker.py +77 -0
- gozu-0.3.7/src/temporal/workflows/__init__.py +6 -0
- gozu-0.3.7/src/temporal/workflows/multi_branch_scan.py +90 -0
- gozu-0.3.7/src/temporal/workflows/reconcile_only.py +54 -0
- gozu-0.3.7/src/temporal/workflows/scan_to_ticket.py +208 -0
- gozu-0.3.7/src/ticket/AtlassianDocumentFormat.py +31 -0
- gozu-0.3.7/src/ticket/TicketClient.py +71 -0
- gozu-0.3.7/src/ticket/__init__.py +3 -0
- gozu-0.3.7/src/ticket/claims.py +134 -0
- gozu-0.3.7/src/ticket/factory.py +52 -0
- gozu-0.3.7/src/ticket/jira_client.py +434 -0
- gozu-0.3.7/src/ticket/jira_sprint.py +82 -0
- gozu-0.3.7/tests/__init__.py +4 -0
- gozu-0.3.7/tests/cli/__init__.py +0 -0
- gozu-0.3.7/tests/cli/test_scan_runner_trivy.py +132 -0
- gozu-0.3.7/tests/cli/test_trivy_prerequisite.py +67 -0
- gozu-0.3.7/tests/github_action/__init__.py +3 -0
- gozu-0.3.7/tests/github_action/test_main.py +203 -0
- gozu-0.3.7/tests/llm/__init__.py +2 -0
- gozu-0.3.7/tests/llm/test_enrich.py +181 -0
- gozu-0.3.7/tests/scanner/__init__.py +3 -0
- gozu-0.3.7/tests/scanner/test_factory.py +37 -0
- gozu-0.3.7/tests/scanner/test_html_text.py +39 -0
- gozu-0.3.7/tests/scanner/test_screenshot.py +202 -0
- gozu-0.3.7/tests/scanner/test_semgrep_local.py +193 -0
- gozu-0.3.7/tests/scanner/test_trivy_client.py +248 -0
- gozu-0.3.7/tests/scanner/test_trivy_integration.py +113 -0
- gozu-0.3.7/tests/temporal/__init__.py +4 -0
- gozu-0.3.7/tests/temporal/activities/__init__.py +4 -0
- gozu-0.3.7/tests/temporal/activities/test_capture_and_attach_screenshot.py +153 -0
- gozu-0.3.7/tests/temporal/activities/test_create_tickets.py +234 -0
- gozu-0.3.7/tests/temporal/activities/test_fetch_findings.py +61 -0
- gozu-0.3.7/tests/ticket/__init__.py +2 -0
- gozu-0.3.7/tests/ticket/test_jira_client.py +314 -0
- gozu-0.3.7/uv.lock +1279 -0
gozu-0.3.7/.dockerignore
ADDED
gozu-0.3.7/.env.example
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Copyright (c) 2026 Calfus Inc.
|
|
2
|
+
# Author: Wasiullah Rafeeq S
|
|
3
|
+
|
|
4
|
+
# --- Phase 1 infrastructure (docker-compose.yml) ---
|
|
5
|
+
# Don't fill these in by hand - run `python src/scripts/bootstrap_env.py` once
|
|
6
|
+
# to generate a real .env with a random Postgres password and a fresh
|
|
7
|
+
# Fernet key. See README.md's "Phase 1: Infrastructure" section.
|
|
8
|
+
POSTGRES_USER=sonarjira
|
|
9
|
+
POSTGRES_PASSWORD=
|
|
10
|
+
POSTGRES_DB=sonarjira
|
|
11
|
+
POSTGRES_HOST=localhost
|
|
12
|
+
POSTGRES_PORT=5432
|
|
13
|
+
|
|
14
|
+
# Fernet key used by crypto_utils.py to encrypt/decrypt secrets stored in
|
|
15
|
+
# the configs table (sonar_token, jira_api_token, webhook_secret).
|
|
16
|
+
FERNET_KEY=
|
|
17
|
+
|
|
18
|
+
# Temporal server address. docker-compose.yml overrides this to
|
|
19
|
+
# temporal:7233 for the worker container.
|
|
20
|
+
TEMPORAL_HOST=localhost:7233
|
|
21
|
+
|
|
22
|
+
# --- SonarQube webhook ---
|
|
23
|
+
# Shared secret configured in the SonarQube webhook settings (Administration > Webhooks).
|
|
24
|
+
# SonarQube signs each webhook payload with this secret and sends the signature in the
|
|
25
|
+
# X-Sonar-Webhook-HMAC-SHA256 header.
|
|
26
|
+
SONAR_WEBHOOK_SECRET=changeme
|
|
27
|
+
|
|
28
|
+
# --- Scanner backend ---
|
|
29
|
+
# "sonarqube" = self-hosted SonarQube Community Build (SonarQubeServerClient)
|
|
30
|
+
# "sonarqube-cloud" = SonarQube Cloud (SonarQubeCloudClient) - not implemented yet
|
|
31
|
+
# (SONAR_MODE "local"/"cloud" is still read as a fallback if this is unset)
|
|
32
|
+
SCANNER_TYPE=sonarqube
|
|
33
|
+
|
|
34
|
+
# Base URL of your SonarQube instance
|
|
35
|
+
SONAR_HOST_URL=http://localhost:9000
|
|
36
|
+
|
|
37
|
+
# User token generated in SonarQube (My Account > Security > Generate Tokens)
|
|
38
|
+
SONAR_TOKEN=
|
|
39
|
+
|
|
40
|
+
# Only used when SONAR_MODE=cloud
|
|
41
|
+
SONAR_ORGANIZATION=
|
|
42
|
+
|
|
43
|
+
# --- Ticket backend ---
|
|
44
|
+
# "jira" = Jira Cloud (JiraClient) - the only implementation right now
|
|
45
|
+
TICKET_BACKEND=jira
|
|
46
|
+
|
|
47
|
+
# --- Jira ---
|
|
48
|
+
# Base URL of your Jira Cloud site, e.g. https://your-domain.atlassian.net
|
|
49
|
+
JIRA_URL=
|
|
50
|
+
|
|
51
|
+
# Email address of the Jira account the API token belongs to
|
|
52
|
+
JIRA_EMAIL=
|
|
53
|
+
|
|
54
|
+
# API token generated at https://id.atlassian.com/manage-profile/security/api-tokens
|
|
55
|
+
JIRA_API_TOKEN=
|
|
56
|
+
|
|
57
|
+
# Key of the Jira project tickets should be created in, e.g. "SONAR"
|
|
58
|
+
JIRA_PROJECT_KEY=
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Copyright (c) 2026 Calfus Inc.
|
|
2
|
+
# Author: Prakrit Mohanty
|
|
3
|
+
# Editor: Wasiullah Rafeeq S
|
|
4
|
+
|
|
5
|
+
name: CI
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
push:
|
|
9
|
+
branches: [main]
|
|
10
|
+
pull_request:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v3
|
|
20
|
+
with:
|
|
21
|
+
version: "latest"
|
|
22
|
+
enable-cache: true
|
|
23
|
+
cache-dependency-glob: "uv.lock"
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
run: uv python install 3.12
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: uv sync
|
|
30
|
+
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: uv run pytest -v
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Copyright (c) 2026 Calfus Inc.
|
|
2
|
+
# Author: Prakrit Mohanty
|
|
3
|
+
# Editor: Wasiullah Rafeeq S
|
|
4
|
+
|
|
5
|
+
name: Release
|
|
6
|
+
|
|
7
|
+
# Triggered by pushing a version tag, e.g.:
|
|
8
|
+
# git tag v0.1.0 && git push origin v0.1.0
|
|
9
|
+
# The tag (minus its leading "v") must match pyproject.toml's [project]
|
|
10
|
+
# version exactly - the version-check step below fails fast if they drift,
|
|
11
|
+
# rather than silently publishing a release with the wrong version string.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
tags:
|
|
16
|
+
- "v*"
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write
|
|
20
|
+
id-token: write
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
build-and-release:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@v3
|
|
30
|
+
with:
|
|
31
|
+
version: "latest"
|
|
32
|
+
enable-cache: true
|
|
33
|
+
cache-dependency-glob: "uv.lock"
|
|
34
|
+
|
|
35
|
+
- name: Set up Python
|
|
36
|
+
run: uv python install 3.12
|
|
37
|
+
|
|
38
|
+
- name: Check tag matches pyproject.toml version
|
|
39
|
+
run: |
|
|
40
|
+
tag_version="${GITHUB_REF_NAME#v}"
|
|
41
|
+
pkg_version="$(uv run python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
|
|
42
|
+
if [ "$tag_version" != "$pkg_version" ]; then
|
|
43
|
+
echo "::error::Tag $GITHUB_REF_NAME (version $tag_version) doesn't match pyproject.toml's version $pkg_version"
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
- name: Install dependencies
|
|
48
|
+
run: uv sync
|
|
49
|
+
|
|
50
|
+
- name: Run tests
|
|
51
|
+
run: uv run pytest -v
|
|
52
|
+
|
|
53
|
+
- name: Build wheel and sdist
|
|
54
|
+
run: uv build
|
|
55
|
+
|
|
56
|
+
# Trusted publishing (OIDC): the "gozu" project on PyPI trusts this
|
|
57
|
+
# repo's release.yml, so no PyPI token/secret is needed - uv exchanges
|
|
58
|
+
# the id-token permission above for a short-lived upload credential.
|
|
59
|
+
# Runs before the GitHub Release so a failed upload leaves no release.
|
|
60
|
+
- name: Publish to PyPI
|
|
61
|
+
run: uv publish
|
|
62
|
+
|
|
63
|
+
- name: Create GitHub Release
|
|
64
|
+
env:
|
|
65
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
66
|
+
run: |
|
|
67
|
+
gh release create "$GITHUB_REF_NAME" dist/* \
|
|
68
|
+
--title "$GITHUB_REF_NAME" \
|
|
69
|
+
--generate-notes
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Copyright (c) 2026 Calfus Inc.
|
|
2
|
+
# Author: Prakrit Mohanty
|
|
3
|
+
|
|
4
|
+
name: Sonar to Jira (main)
|
|
5
|
+
|
|
6
|
+
# Reacts to SonarQube Cloud's Automatic Analysis (which scans `main` on
|
|
7
|
+
# every push automatically, no scan step needed here - see
|
|
8
|
+
# docs.sonarsource.com/sonarqube-cloud/analyzing-source-code/automatic-analysis)
|
|
9
|
+
# by waiting for that analysis to land, then creating Jira tickets for new
|
|
10
|
+
# findings - see src/github_action/main.py.
|
|
11
|
+
#
|
|
12
|
+
# Deliberately scoped to `main` only: SonarQube Cloud's Free plan rejects
|
|
13
|
+
# querying issue data for any other branch ("Organization is not allowed
|
|
14
|
+
# to access data from non main branches" - confirmed live, see
|
|
15
|
+
# src/cli/scan_runner/scanner_exec.py's comment), so a PR-time equivalent of
|
|
16
|
+
# this workflow cannot work on Free.
|
|
17
|
+
#
|
|
18
|
+
# Runs entirely inside GitHub's own runner - unlike gozu's webhook trigger
|
|
19
|
+
# mode, this needs no receiver reachable from the public internet, only
|
|
20
|
+
# outbound calls to SonarQube Cloud's and Jira's APIs.
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
branches: [main]
|
|
25
|
+
|
|
26
|
+
concurrency:
|
|
27
|
+
group: sonar-to-jira-main
|
|
28
|
+
cancel-in-progress: false
|
|
29
|
+
|
|
30
|
+
jobs:
|
|
31
|
+
create-tickets:
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- name: Set up Python
|
|
37
|
+
uses: actions/setup-python@v5
|
|
38
|
+
with:
|
|
39
|
+
python-version: "3.12"
|
|
40
|
+
|
|
41
|
+
# Installs from this exact checkout, not a GitHub Release URL -
|
|
42
|
+
# this repo is private, and pip has no credentials to fetch a
|
|
43
|
+
# private repo's release asset (confirmed live: a plain
|
|
44
|
+
# `pip install <release url>` 404s here even though the release
|
|
45
|
+
# itself exists - GitHub returns 404, not 401/403, for a private
|
|
46
|
+
# resource fetched unauthenticated). Installing from the checkout
|
|
47
|
+
# also means this never drifts from whatever version is actually on
|
|
48
|
+
# main. Swap for `pip install gozu` once this is published to PyPI.
|
|
49
|
+
- name: Install gozu
|
|
50
|
+
run: pip install .
|
|
51
|
+
|
|
52
|
+
- name: Wait for analysis and create Jira tickets
|
|
53
|
+
env:
|
|
54
|
+
SCANNER_TYPE: sonarqube-cloud
|
|
55
|
+
SONAR_HOST_URL: https://sonarcloud.io
|
|
56
|
+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
57
|
+
SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }}
|
|
58
|
+
SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }}
|
|
59
|
+
TICKET_BACKEND: jira
|
|
60
|
+
JIRA_URL: ${{ secrets.JIRA_URL }}
|
|
61
|
+
JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }}
|
|
62
|
+
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
|
|
63
|
+
JIRA_PROJECT_KEY: ${{ secrets.JIRA_PROJECT_KEY }}
|
|
64
|
+
COMMIT_TIMESTAMP: ${{ github.event.head_commit.timestamp }}
|
|
65
|
+
# Optional - blank/unset disables LLM-generated ticket explanations
|
|
66
|
+
# for Blocker/Critical/High findings (see src/llm/enrich.py); tickets
|
|
67
|
+
# fall back to today's templated description either way.
|
|
68
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
69
|
+
run: gozu-github-action
|
gozu-0.3.7/.gitignore
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright (c) 2026 Calfus Inc.
|
|
2
|
+
# Author: Wasiullah Rafeeq S
|
|
3
|
+
|
|
4
|
+
.env
|
|
5
|
+
docker-compose.override.yml
|
|
6
|
+
.venv/
|
|
7
|
+
venv/
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.pyc
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.scannerwork/
|
|
12
|
+
|
|
13
|
+
# ARCHITECTURE.md and Setup.md live here now - kept on disk for local
|
|
14
|
+
# reference but no longer tracked (README.md/CLAUDE.md stay at repo root,
|
|
15
|
+
# tracked, since GitHub's repo homepage and Claude Code's own context
|
|
16
|
+
# loading both depend on those two specific paths).
|
|
17
|
+
docs/
|
gozu-0.3.7/CLAUDE.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Project conventions
|
|
2
|
+
|
|
3
|
+
## The product
|
|
4
|
+
|
|
5
|
+
This repo is being built into a commercial CLI tool called **Gozu** (the
|
|
6
|
+
name is decided, not a placeholder - use it consistently: the command is
|
|
7
|
+
`gozu`, host-side dirs like `~/.gozu/`). The repo/working directory stays
|
|
8
|
+
`sonar-to-jira` for now - don't rename it without checking first, since
|
|
9
|
+
that's a separate decision from the product's own name.
|
|
10
|
+
It's scanner- and ticket-backend-agnostic by design: users scan code with
|
|
11
|
+
a pluggable choice of scanners (currently SonarQube, Trivy, and Semgrep) and get
|
|
12
|
+
tickets auto-created in a pluggable choice of ticket-tracking backends
|
|
13
|
+
(currently Jira), orchestrated with Temporal. These are the first concrete
|
|
14
|
+
implementations, not the definition of the tool - don't write new
|
|
15
|
+
docs/help text that implies gozu is confined to either. The build is happening in
|
|
16
|
+
phases (Phase 1: Docker Compose infra + encrypted config store. Phase 2:
|
|
17
|
+
`gozu init`, the setup wizard - see README.md for both); check with
|
|
18
|
+
the user before assuming a later phase's scope (e.g. `gozu run`,
|
|
19
|
+
actually running a scan) is open to work on.
|
|
20
|
+
|
|
21
|
+
## CLI conventions (Phase 2+)
|
|
22
|
+
|
|
23
|
+
- The `src/cli/` package holds everything CLI-specific: `main.py` (the typer
|
|
24
|
+
app), `init_wizard/`, `scan_runner/`, `prerequisites/` (each a small
|
|
25
|
+
package, one file per step/concern - see their own docstrings), plus
|
|
26
|
+
`help_links.py` and `stack.py`.
|
|
27
|
+
- Every interactive select/confirm prompt uses **questionary** (arrow-key
|
|
28
|
+
menus), not typed option strings or plain `input()` - this is a fixed
|
|
29
|
+
choice for this project, not a per-prompt toss-up, given how many
|
|
30
|
+
branching selects the wizard has.
|
|
31
|
+
- Packaging is **uv**, not pip/`requirements.txt` (removed). Dependencies
|
|
32
|
+
live in `pyproject.toml`'s `[project.dependencies]`, pinned via
|
|
33
|
+
`uv.lock`. Install with `uv sync`; run things with `uv run <cmd>` or an
|
|
34
|
+
activated `.venv`. The worker `Dockerfile` builds via `uv sync --frozen`
|
|
35
|
+
against the same `pyproject.toml`/`uv.lock`, not a separate
|
|
36
|
+
requirements file - keep it that way rather than hand-maintaining two
|
|
37
|
+
dependency lists.
|
|
38
|
+
- A credential/doc-link helper (`src/cli/help_links.py`'s `HELP_LINKS`) must
|
|
39
|
+
use real, current URLs looked up live (WebSearch or equivalent) - never
|
|
40
|
+
a plausible-looking guessed URL.
|
|
41
|
+
|
|
42
|
+
## Naming: "configs", not "profiles"
|
|
43
|
+
|
|
44
|
+
A saved, named set of scanner + ticket-backend credentials (see
|
|
45
|
+
`db/migrations/versions/0001_initial_schema.py`'s `configs` table, `src/config/store.py`) is called a **config**,
|
|
46
|
+
never a "profile". Docker Compose already has an unrelated built-in concept
|
|
47
|
+
called profiles (`docker-compose.yml`'s `profiles: [...]` on the
|
|
48
|
+
`sonarqube` service, for conditionally starting services) - reusing "profile"
|
|
49
|
+
for credential sets would be confusing throughout the codebase and docs
|
|
50
|
+
given both concepts exist side by side in this project.
|
|
51
|
+
|
|
52
|
+
## Data models
|
|
53
|
+
|
|
54
|
+
Use **Pydantic `BaseModel`** for every data model in this project - never
|
|
55
|
+
`dataclasses.dataclass` or plain classes. This includes types that cross a
|
|
56
|
+
Temporal workflow/activity boundary (`Finding`, `CreatedTicket`,
|
|
57
|
+
`TicketResult`, `SonarToJiraInput`, `ScreenshotAttachInput`) and any new
|
|
58
|
+
ones added later (e.g. `ScannerRequirements`, `FindingExtraction`). The
|
|
59
|
+
Pydantic-aware data converter (`src/temporal/data_converter.py`) serializes
|
|
60
|
+
`BaseModel`s across that boundary automatically - a plain dataclass or
|
|
61
|
+
dict does not, and needs manual conversion, which is exactly what we
|
|
62
|
+
removed by standardizing on Pydantic.
|
|
63
|
+
|
|
64
|
+
## Logging
|
|
65
|
+
|
|
66
|
+
Use **exactly one** logging mechanism inside the Temporal-executed pipeline:
|
|
67
|
+
|
|
68
|
+
- `workflow.logger` (from `temporalio.workflow`) inside workflow code
|
|
69
|
+
(`src/temporal/workflows/`).
|
|
70
|
+
- `activity.logger` (from `temporalio.activity`) everywhere else that runs
|
|
71
|
+
inside an activity - the activity functions themselves
|
|
72
|
+
(`src/temporal/activities/`) and every module they call into
|
|
73
|
+
(`src/scanner/`, `src/ticket/`).
|
|
74
|
+
|
|
75
|
+
Both are Temporal's contextual loggers - they tag every line with
|
|
76
|
+
workflow/activity id, run id, attempt number, etc, and route to the same
|
|
77
|
+
place `workflow.logger`/`activity.logger` already do. Don't create a plain
|
|
78
|
+
`logging.getLogger(__name__)` logger in any file that runs inside an
|
|
79
|
+
activity or workflow.
|
|
80
|
+
|
|
81
|
+
The only exception is the two process entrypoints, which run before any
|
|
82
|
+
Temporal workflow/activity context exists: `src/receiver/app.py` (Flask, before
|
|
83
|
+
a workflow is started) and `src/temporal/worker.py` (bootstrap, before
|
|
84
|
+
`worker.run()`). Those use plain `logging.basicConfig`/`logging.getLogger`
|
|
85
|
+
because there's nothing to attach Temporal context to yet.
|
|
86
|
+
|
|
87
|
+
Do not introduce a third logging approach (e.g. `print`, a custom logger
|
|
88
|
+
wrapper, structlog, etc) anywhere in this project.
|
|
89
|
+
|
|
90
|
+
## Code style: modular, reusable, no needless duplication
|
|
91
|
+
|
|
92
|
+
Favor small, single-responsibility functions over long ones, and pull out
|
|
93
|
+
a shared helper the moment the same shape of logic appears twice - don't
|
|
94
|
+
wait for a third copy. Concretely, follow the patterns already in this
|
|
95
|
+
codebase rather than inventing new ones:
|
|
96
|
+
|
|
97
|
+
- **Pure-builder + env-reading-wrapper split**, for anything that can be
|
|
98
|
+
configured either explicitly or from the environment: `src/scanner/factory.py`'s
|
|
99
|
+
`build_scanner_client(scanner_type, scanner_mode, credentials)` (no env
|
|
100
|
+
reads) vs `get_scanner_client()` (reads env vars, delegates to the
|
|
101
|
+
builder); `src/ticket/factory.py`'s `build_ticket_client()`/`get_ticket_client()`
|
|
102
|
+
mirror it. When adding a new backend or a new per-config code path, add
|
|
103
|
+
to the pure builder and let the env-reading wrapper stay a thin
|
|
104
|
+
translation layer - don't duplicate the branching in both.
|
|
105
|
+
- **Extract shared download/verify logic**, not per-tool copies: see
|
|
106
|
+
`src/cli/prerequisites/archive.py`'s `download_archive()`/`extract_archive()`/
|
|
107
|
+
`make_tree_executable()`/`verify_runnable()`, shared by `java.py`'s
|
|
108
|
+
`ensure_java()` and `sonar_scanner.py`'s `ensure_sonar_scanner()`. A third
|
|
109
|
+
`ensure_*()` for a new host dependency should reuse these, not reimplement
|
|
110
|
+
its own download loop. Same idea for small derived values used in more
|
|
111
|
+
than one place - e.g. `src/cli/scan_runner/config_fields.py`'s
|
|
112
|
+
`scanner_host_url(config)`, computed once and reused rather than
|
|
113
|
+
recomputed per call site.
|
|
114
|
+
- **Dedicated, narrow Pydantic models per activity boundary** (interface
|
|
115
|
+
segregation) rather than one bloated shared input model: `fetch_findings_activity`
|
|
116
|
+
takes `FetchFindingsInput`, `create_tickets_activity` takes
|
|
117
|
+
`CreateTicketsInput`, not the full `SonarToJiraInput` - each activity
|
|
118
|
+
only sees the fields it actually needs.
|
|
119
|
+
|
|
120
|
+
This isn't a call for premature abstraction - three near-identical lines
|
|
121
|
+
inline are still fine. It's specifically about not letting the same
|
|
122
|
+
piece of logic (a download-and-verify flow, a client-construction
|
|
123
|
+
branch, a derived value) exist in more than one place at a time.
|
|
124
|
+
|
|
125
|
+
## File headers: copyright/author on every file, every machine
|
|
126
|
+
|
|
127
|
+
Every source file in this repo carries a header, added retroactively in
|
|
128
|
+
one pass and required on every new file since - this must happen the same
|
|
129
|
+
way regardless of who's working or which machine, not something to
|
|
130
|
+
remember case-by-case:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
# Copyright (c) 2026 Calfus Inc.
|
|
134
|
+
# Author: <original author's real name>
|
|
135
|
+
# Editor: <anyone else who has since modified this file, comma-separated>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
No license line - ownership is still being confirmed with the company;
|
|
139
|
+
don't add one until that's resolved. Comment syntax matches the file
|
|
140
|
+
(`#` for Python/YAML/TOML/Dockerfile/plain config files, `--` for
|
|
141
|
+
`.sql` files, `//` for anything C-style). Place it after a
|
|
142
|
+
shebang line where one exists (the shebang must stay the very first
|
|
143
|
+
line), before everything else - including a module docstring - otherwise.
|
|
144
|
+
|
|
145
|
+
- **Author** is whoever actually wrote the file first - for a brand new
|
|
146
|
+
file being created in this session, that's the user driving the
|
|
147
|
+
session, by their real name (check the git identity/context for this
|
|
148
|
+
repo rather than guessing or using "Claude"). Never invent a name.
|
|
149
|
+
- **Editor** is anyone else who has since modified that same file -
|
|
150
|
+
omit the line entirely if there isn't one yet. When someone other than
|
|
151
|
+
the original author changes an existing file, add their name to the
|
|
152
|
+
Editor line if it isn't already there (comma-separate multiple
|
|
153
|
+
editors); don't touch Author once it's set. Same rule as Author:
|
|
154
|
+
real people only, by their real name (check the git identity/context
|
|
155
|
+
for this repo) - never "Claude", even when Claude is the one literally
|
|
156
|
+
typing the edit. Claude is a tool the person is using, not a credited
|
|
157
|
+
contributor. If the resulting editor would be the same person as
|
|
158
|
+
Author, don't add an Editor line at all - that's already covered by
|
|
159
|
+
Author.
|
|
160
|
+
- **Dependency line** (optional, added under a blank `#`): a factual,
|
|
161
|
+
one-line "Depends on: X (Vendor) - what for" note, ONLY for a file that
|
|
162
|
+
directly talks to one of the four things this product's identity
|
|
163
|
+
depends on - SonarQube (SonarSource), Jira (Atlassian), Temporal
|
|
164
|
+
(Temporal Technologies), or PostgreSQL - and only when that file is
|
|
165
|
+
actually doing the talking (a direct API/SDK call), not just adjacent
|
|
166
|
+
to one. No language implying partnership, endorsement, or certification
|
|
167
|
+
by the third party - state the dependency, nothing more. Most files get
|
|
168
|
+
no dependency line at all; don't force one in where it doesn't apply.
|
|
169
|
+
- **In-house dependency line(s)** (same block, one line per dependency):
|
|
170
|
+
a file with a genuine load-bearing dependency on another module
|
|
171
|
+
*within this repo* also gets its own "Depends on: <repo-relative file
|
|
172
|
+
path> - what for" line - e.g. "Depends on: src/config/store.py - reads/
|
|
173
|
+
writes the configs table". Repo-relative file path, not an import
|
|
174
|
+
statement or module dotted-path. Same bar as the vendor line above:
|
|
175
|
+
only the specific module(s) a file is actually built on top of to do
|
|
176
|
+
its own job (what it calls into), not every module it happens to
|
|
177
|
+
import - a shared status/glyph/constants helper doesn't count. One
|
|
178
|
+
line per dependency when there's more than one. Most files still get
|
|
179
|
+
none.
|
|
180
|
+
- Markdown files (`README.md`, `ARCHITECTURE.md`, this file) are
|
|
181
|
+
deliberately excluded - no comment syntax was ever specified for them,
|
|
182
|
+
and guessing one (e.g. an HTML comment block) isn't this convention's
|
|
183
|
+
call to make unilaterally. Generated/vendored files (e.g. `uv.lock`)
|
|
184
|
+
are excluded too - never hand-edit those anyway.
|
|
185
|
+
- Skip this convention only if the user explicitly asks to - never
|
|
186
|
+
silently omit a header because a file feels too small or too generated
|
|
187
|
+
to bother.
|
gozu-0.3.7/Dockerfile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright (c) 2026 Calfus Inc.
|
|
2
|
+
# Author: Wasiullah Rafeeq S
|
|
3
|
+
|
|
4
|
+
FROM ghcr.io/astral-sh/uv:latest AS uv
|
|
5
|
+
|
|
6
|
+
FROM python:3.12-slim
|
|
7
|
+
|
|
8
|
+
ENV PYTHONUNBUFFERED=1
|
|
9
|
+
ENV PYTHONPATH=/app/src
|
|
10
|
+
ENV UV_PROJECT_ENVIRONMENT=/usr/local
|
|
11
|
+
ENV UV_LINK_MODE=copy
|
|
12
|
+
|
|
13
|
+
WORKDIR /app
|
|
14
|
+
|
|
15
|
+
COPY --from=uv /uv /uvx /usr/local/bin/
|
|
16
|
+
|
|
17
|
+
# src/scanner/screenshot.py's render_finding_snippet() needs Pygments'
|
|
18
|
+
# ImageFormatter to find an actual monospace font - confirmed live that
|
|
19
|
+
# python:3.12-slim has neither fontconfig (`fc-list`) nor any font at
|
|
20
|
+
# all, which makes ImageFormatter raise FileNotFoundError outright, not
|
|
21
|
+
# just render ugly. fonts-dejavu-core is the specific package that
|
|
22
|
+
# provides DejaVu Sans Mono, the font ImageFormatter looks for first;
|
|
23
|
+
# both packages together add a few MB, nowhere near what the Chromium
|
|
24
|
+
# install this replaced used to cost.
|
|
25
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
26
|
+
fontconfig fonts-dejavu-core \
|
|
27
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
28
|
+
|
|
29
|
+
COPY pyproject.toml uv.lock ./
|
|
30
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
31
|
+
uv sync --frozen --no-install-project --no-group dev
|
|
32
|
+
|
|
33
|
+
COPY . .
|
|
34
|
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
|
35
|
+
uv sync --frozen --no-group dev
|
|
36
|
+
|
|
37
|
+
CMD ["python", "-m", "temporal.worker"]
|