Skip to content
Merged
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
86 changes: 69 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ docker compose run --rm codacy-ai

Required env vars: `CODACY_API_TOKEN`, and `ANTHROPIC_API_KEY` or `GEMINI_API_KEY` (or both).

> **Docker Desktop users:** set Memory to at least 12 GB in Settings → Resources → Memory. Analysis peaks at ~10.5 GB.
The repository at `SOURCE_PATH` must already be on Codacy Cloud with at least one finished analysis. The container tunes
the cloud configuration via Cloud reanalysis — it does not run local analysis, and it does not import not-yet-on-Codacy
repositories.

Or from any folder, without the compose file:

```bash
docker run --rm -it \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
--device /dev/kmsg:/dev/kmsg \
--memory=12g --memory-swap=12g \
-v codacy-tool-cache:/home/node/.codacy \
-v $(pwd):/workspace \
-e CODACY_API_TOKEN -e ANTHROPIC_API_KEY -e GEMINI_API_KEY \
Expand All @@ -29,35 +30,86 @@ Or with an explicit env file:
docker run --rm -it \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
--device /dev/kmsg:/dev/kmsg \
--memory=12g --memory-swap=12g \
-v codacy-tool-cache:/home/node/.codacy \
-v $(pwd):/workspace \
--env-file /path/to/.env \
--env-file ./../.env \
codacy/autoconfig
Comment on lines 33 to 36
```

| Flag | Purpose |
|---|---|
| `--rm` | Delete the container on exit |
| `-it` | Interactive terminal |
| `--cap-add=NET_ADMIN --cap-add=NET_RAW` | Required to enforce the outbound firewall inside the container |
| `--device /dev/kmsg:/dev/kmsg` | Kernel device needed by the firewall setup |
| `--memory=12g --memory-swap=12g` | Cap memory at 12 GB, no swap (analysis peaks ~10.5 GB) |
| `-v codacy-tool-cache:/home/node/.codacy` | Persistent volume so downloaded tools survive between runs |
| `-v $(pwd):/workspace` | Mounts your current folder as `/workspace` |
| `-e ...` | Passes API tokens from your host environment into the container |
| `--env-file /path/to/.env` | Alternative to `-e` flags — loads vars from a file |
| Flag | Purpose |
|-------------------------------------------|-----------------------------------------------------------------|
| `--rm` | Delete the container on exit |
| `-it` | Interactive terminal |
| `--cap-add=NET_ADMIN --cap-add=NET_RAW` | Required to enforce the outbound firewall inside the container |
| `--device /dev/kmsg:/dev/kmsg` | Kernel device needed by the firewall block-log stream |
| `-v codacy-tool-cache:/home/node/.codacy` | Persistent volume so downloaded tools survive between runs |
| `-v $(pwd):/workspace` | Mounts your current folder as `/workspace` |
| `-e ...` | Passes API tokens from your host environment into the container |
| `--env-file /path/to/.env` | Alternative to `-e` flags — loads vars from a file |

To rebuild the image:

```bash
docker compose build
```

## Two pipelines, local and server

The image ships two entrypoint scripts:

- `local-pipeline.sh` (default). For developers running the container against a mounted source folder. Used by
`docker compose` and the `docker run` examples above. Invokes `/configure-codacy-cloud` against `/workspace`.
- `server-pipeline.sh`. For the Active Analysis Manager (AAM) in production. Clones the repository via `GIT_TOKEN`,
invokes `/configure-codacy-cloud`, and uploads a JSONL summary to a presigned S3 URL. The clone URL is built per
provider (`CODACY_PROVIDER` of `gh`/`ghe` for GitHub, `gl`/`gle` for GitLab, `bb` for Bitbucket).
Comment on lines +58 to +64

Both scripts run the same skill. The skill tunes a repository's Codacy Cloud configuration via Cloud reanalysis and
never runs local static analysis tools — that's why the container's egress allowlist is narrow (Claude, Gemini, Codacy).

To test `server-pipeline.sh` locally, override the entrypoint and provide the additional env vars. Note that the
local firewall does not allow git provider hosts, so set `RUNNING_IN_K8S=true` to skip it for this test:

```bash
docker run --rm -it \
-v codacy-tool-cache:/home/node/.codacy \
-e RUNNING_IN_K8S=true \
-e CODACY_API_TOKEN \
-e ANTHROPIC_API_KEY \
-e GIT_TOKEN \
-e CODACY_PROVIDER=gh \
-e CODACY_ORG_NAME=your-org \
-e CODACY_REPO_NAME=your-repo \
-e RESULT_UPLOAD_URL=https://httpbin.org/put \
--entrypoint /usr/local/bin/server-pipeline.sh \
codacy/autoconfig
```

`httpbin.org/put` accepts any PUT and is useful for smoke-testing the upload step.

To capture the summary on your host instead of sending it to httpbin, run a tiny HTTP sink in another terminal:

```bash
python3 -c "
import http.server
class H(http.server.BaseHTTPRequestHandler):
def do_PUT(self):
n = int(self.headers.get('Content-Length', 0))
open('summary.received.jsonl', 'wb').write(self.rfile.read(n))
self.send_response(200); self.end_headers()
http.server.HTTPServer(('0.0.0.0', 8080), H).serve_forever()
"
```

Then point the container at it with `RESULT_UPLOAD_URL=http://host.docker.internal:8080/upload`.

Required env vars for the server pipeline: `CODACY_API_TOKEN`, `ANTHROPIC_API_KEY`, `GIT_TOKEN`, `CODACY_PROVIDER`,
`CODACY_ORG_NAME`, `CODACY_REPO_NAME`, `RESULT_UPLOAD_URL`. The script fails fast if any are missing.

## What's inside

- `codacy` — Codacy Cloud CLI
- `codacy-analysis` — runs static analysis tools locally (trivy, ruff, opengrep, pmd, checkov, etc., downloaded on first use)
- `codacy-analysis` — Codacy Analysis CLI (used by the skill only for config-file operations)
- `claude` / `gemini` — AI assistants
- Java 21, Python 3.12, Ruby, Go 1.26, shellcheck
- Outbound firewall — allowlist only (GitHub, Codacy, Anthropic, Google, npm, PyPI)
- Outbound firewall — allowlist for Claude, Gemini, and Codacy only. In production (k8s) the firewall is skipped and
egress is enforced by NetworkPolicy at the cluster level instead.
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ services:
cap_add:
- NET_ADMIN
- NET_RAW
mem_limit: 12g
memswap_limit: 12g
devices:
- /dev/kmsg:/dev/kmsg
volumes:
Expand Down
12 changes: 8 additions & 4 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,17 @@ RUN npm install -g \
@codacy/codacy-cloud-cli \
@codacy/analysis-cli

# Firewall init — node user gets sudo rights for this script only
# Pre-bake skills — Claude loads via --plugin-dir, Gemini installs from local path
# ADD'ing the master ref content makes Docker invalidate this layer whenever codacy-skills master moves,
# so a fresh `docker build` always gets the latest skills without --no-cache.
ADD https://api.github.com/repos/codacy/codacy-skills/git/refs/heads/master /tmp/codacy-skills-ref
RUN git clone --depth 1 https://github.com/codacy/codacy-skills.git /opt/codacy-skills
Comment on lines 45 to 49

COPY docker/init-firewall.sh /usr/local/bin/init-firewall.sh
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
COPY docker/pipeline.sh /usr/local/bin/pipeline.sh
RUN chmod +x /usr/local/bin/init-firewall.sh /usr/local/bin/entrypoint.sh /usr/local/bin/pipeline.sh \
COPY docker/local-pipeline.sh /usr/local/bin/local-pipeline.sh
COPY docker/server-pipeline.sh /usr/local/bin/server-pipeline.sh
RUN chmod +x /usr/local/bin/init-firewall.sh /usr/local/bin/entrypoint.sh /usr/local/bin/local-pipeline.sh /usr/local/bin/server-pipeline.sh \
&& printf 'node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh\nnode ALL=(root) NOPASSWD: /bin/chown -R node\\:node /home/node/.codacy\n' \
> /etc/sudoers.d/node-firewall \
&& chmod 0440 /etc/sudoers.d/node-firewall
Expand All @@ -61,11 +64,12 @@ USER node
COPY --chown=node:node docker/claude-settings.json /home/node/.claude/settings.json
RUN mkdir -p /home/node/.claude/commands/references \
&& cp /opt/codacy-skills/skills/configure-codacy/SKILL.md /home/node/.claude/commands/configure-codacy.md \
&& cp /opt/codacy-skills/skills/configure-codacy-cloud/SKILL.md /home/node/.claude/commands/configure-codacy-cloud.md \
&& cp /opt/codacy-skills/skills/codacy-analysis-cli/SKILL.md /home/node/.claude/commands/codacy-analysis-cli.md \
&& cp /opt/codacy-skills/skills/codacy-cloud-cli/SKILL.md /home/node/.claude/commands/codacy-cloud-cli.md \
&& cp /opt/codacy-skills/skills/codacy-analysis-cli/references/* /home/node/.claude/commands/references/

WORKDIR /workspace

ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["pipeline.sh"]
CMD ["local-pipeline.sh"]
6 changes: 5 additions & 1 deletion docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#!/bin/bash
set -e

sudo /usr/local/bin/init-firewall.sh
# In k8s, egress is controlled by NetworkPolicy; the in-container iptables firewall
# requires NET_ADMIN and is not available. Skip it when RUNNING_IN_K8S is set.
if [ -z "${RUNNING_IN_K8S:-}" ]; then
sudo /usr/local/bin/init-firewall.sh
fi

# Fix ownership of the tool-cache volume (mounted as root by Docker)
sudo chown -R node:node /home/node/.codacy 2>/dev/null || true
Expand Down
38 changes: 12 additions & 26 deletions docker/init-firewall.sh
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
#!/bin/bash
# Minimal egress allowlist for the container. Three categories only.
# - Claude (api.anthropic.com, statsig.anthropic.com)
# - Gemini (generativelanguage.googleapis.com, oauth2.googleapis.com)
# - Codacy API (api.codacy.com, app.codacy.com)
# Designed for the configure-codacy-cloud flow which makes no local analysis calls.
# To test server-pipeline.sh locally (which needs git clone egress), set RUNNING_IN_K8S=true
# to skip this firewall and rely on the developer's host firewall instead.

set -euo pipefail
IFS=$'\n\t'

Expand All @@ -21,43 +29,22 @@ if [ -n "$DOCKER_DNS_RULES" ]; then
echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
fi

# Protocol-level rules (set before the default-DROP policy)
# Protocol-level rules
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A INPUT -p udp --sport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Build the allowlist
ipset create allowed-domains hash:net

# GitHub IP ranges — covers api.github.com, github.com, raw content, AND ghcr.io (packages)
# ghcr.io (Trivy vulnerability DB) lives under the packages CIDR block
gh_ranges=$(curl -s https://api.github.com/meta)
while read -r cidr; do
ipset add allowed-domains "$cidr" 2>/dev/null || true
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git + .packages)[]' | aggregate -q)

# Resolve each domain to IPs at startup and add to the allowlist
for domain in \
"registry.npmjs.org" \
"api.anthropic.com" \
"statsig.anthropic.com" \
"statsig.com" \
"sentry.io" \
"app.codacy.com" \
"api.codacy.com" \
"generativelanguage.googleapis.com" \
"oauth2.googleapis.com" \
"cveawg.mitre.org" \
"api.osv.dev" \
"pypi.org" \
"files.pythonhosted.org" \
"pkg-containers.githubusercontent.com" \
"bc-api.bridgecrew.io" \
"bc-api.prismacloud.io" \
"cdn.prismacloud.io"; do
"api.codacy.com" \
"app.codacy.com"; do
for _ in 1 2 3 4 5; do
ips=$(dig +noall +answer A "$domain" | awk '$4 == "A" { print $5 }')
while read -r ip; do
Expand Down Expand Up @@ -92,11 +79,10 @@ iptables -A OUTPUT -j REJECT --reject-with icmp-admin-prohibited

# Sanity checks
curl --connect-timeout 5 https://example.com >/dev/null 2>&1 && { echo "FIREWALL ERROR: example.com should be blocked"; exit 1; }
curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1 || { echo "FIREWALL ERROR: api.github.com should be reachable"; exit 1; }
curl --connect-timeout 5 https://api.anthropic.com >/dev/null 2>&1 || true # 401 is fine, blocked is not
curl --connect-timeout 5 https://app.codacy.com >/dev/null 2>&1 || { echo "FIREWALL ERROR: app.codacy.com should be reachable"; exit 1; }

echo "Firewall initialized."
echo "Firewall initialized (claude + gemini + codacy)."

# Emit blocked outbound connections to stderr in real time.
# /dev/kmsg must be mapped into the container: --device /dev/kmsg:/dev/kmsg
Expand Down
24 changes: 24 additions & 0 deletions docker/local-pipeline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
# Local pipeline: tunes an already-on-Codacy repository's cloud config from a mounted source folder.
# Runs the /configure-codacy-cloud skill, which uses Codacy Cloud reanalysis (no local analysis tools).
# Requirement: the repo at /workspace must already be on Codacy with at least one finished analysis.
set -e

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In local-pipeline.sh, set -e is enabled but pipefail is not. This means if claude fails but jq succeeds in the pipeline (lines 11-15), the script will still exit with a success status (0), silently hiding failures. Enabling pipefail ensures that the pipeline's exit status reflects any failure in the command chain.

Suggested change
set -e
set -eo pipefail


cd /workspace

if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
echo "==> Running configure-codacy-cloud with Claude..."
claude -p "/configure-codacy-cloud" \
--output-format stream-json \
--verbose \
--include-partial-messages \
| jq --unbuffered -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'

elif [ -n "${GEMINI_API_KEY:-}" ]; then
echo "==> Running configure-codacy-cloud with Gemini..."
echo "/configure-codacy-cloud" | gemini

else
echo "Error: neither ANTHROPIC_API_KEY nor GEMINI_API_KEY is set." >&2
exit 1
fi
21 changes: 0 additions & 21 deletions docker/pipeline.sh

This file was deleted.

Loading