updating security scan as required before build and issue creation for findings
Security / security (push) Failing after 54s
Security / security (push) Failing after 54s
This commit is contained in:
@@ -1,31 +1,25 @@
|
|||||||
name: Build and Push Docker Image
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_run:
|
||||||
branches:
|
workflows:
|
||||||
- "**"
|
- Security
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
if: ${{ gitea.event.workflow_run.event == 'push' && gitea.event.workflow_run.conclusion == 'success' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
# - name: Run Trivy vulnerability scanner in repo mode
|
ref: ${{ gitea.event.workflow_run.head_sha }}
|
||||||
# uses: aquasecurity/trivy-action@v0.36.0
|
|
||||||
# with:
|
|
||||||
# scan-type: 'fs'
|
|
||||||
# ignore-unfixed: true
|
|
||||||
# format: 'sarif'
|
|
||||||
# output: 'trivy-results.sarif'
|
|
||||||
# severity: 'CRITICAL'
|
|
||||||
|
|
||||||
# - name: Upload Trivy scan results to GitHub Security tab
|
|
||||||
# uses: github/codeql-action/upload-sarif@v4
|
|
||||||
# with:
|
|
||||||
# sarif_file: 'trivy-results.sarif'
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
@@ -39,7 +33,7 @@ jobs:
|
|||||||
- name: Compute image tag
|
- name: Compute image tag
|
||||||
id: tag
|
id: tag
|
||||||
run: |
|
run: |
|
||||||
BRANCH="${{ gitea.ref_name }}"
|
BRANCH="${{ gitea.event.workflow_run.head_branch }}"
|
||||||
|
|
||||||
if [ "$BRANCH" = "main" ]; then
|
if [ "$BRANCH" = "main" ]; then
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
@@ -49,7 +43,8 @@ jobs:
|
|||||||
TAG="test"
|
TAG="test"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "tag=$TAG" >> $GITHUB_OUTPUT
|
echo "tag=$TAG" >> "$GITEA_OUTPUT"
|
||||||
|
echo "branch=$BRANCH" >> "$GITEA_OUTPUT"
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -66,7 +61,7 @@ jobs:
|
|||||||
-d "{
|
-d "{
|
||||||
\"tags\": \"all\",
|
\"tags\": \"all\",
|
||||||
\"title\": \"Gitea Build Succeeded\",
|
\"title\": \"Gitea Build Succeeded\",
|
||||||
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ gitea.ref_name }}\\nImage tag built successfully\"
|
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ steps.tag.outputs.branch }}\\nImage tag built successfully\"
|
||||||
}" \
|
}" \
|
||||||
${{ secrets.APPRISE_URL }}
|
${{ secrets.APPRISE_URL }}
|
||||||
|
|
||||||
@@ -78,6 +73,6 @@ jobs:
|
|||||||
-d "{
|
-d "{
|
||||||
\"tags\": \"all\",
|
\"tags\": \"all\",
|
||||||
\"title\": \"Gitea Build Failed\",
|
\"title\": \"Gitea Build Failed\",
|
||||||
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ gitea.ref_name }}\\nCheck logs in Gitea\"
|
\"body\": \"Repo: ${{ gitea.repository }}\\nBranch: ${{ steps.tag.outputs.branch }}\\nCheck logs in Gitea\"
|
||||||
}" \
|
}" \
|
||||||
${{ secrets.APPRISE_URL }}
|
${{ secrets.APPRISE_URL }}
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "**"
|
- "**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security:
|
security:
|
||||||
@@ -14,17 +19,26 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Prepare security reports
|
||||||
|
run: |
|
||||||
|
mkdir -p security-results
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
# GITLEAKS
|
# GITLEAKS
|
||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
- name: Gitleaks
|
- name: Gitleaks
|
||||||
run: |
|
run: |
|
||||||
|
set +e
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v ${{ github.workspace }}:/repo \
|
-v ${{ gitea.workspace }}:/repo \
|
||||||
ghcr.io/gitleaks/gitleaks:latest \
|
ghcr.io/gitleaks/gitleaks:latest \
|
||||||
detect \
|
detect \
|
||||||
--source /repo
|
--source /repo \
|
||||||
|
--report-format json \
|
||||||
|
--report-path /repo/security-results/gitleaks.json \
|
||||||
|
--exit-code 1
|
||||||
|
echo "$?" > security-results/gitleaks.exit
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
# SEMGREP
|
# SEMGREP
|
||||||
@@ -32,12 +46,17 @@ jobs:
|
|||||||
|
|
||||||
- name: Semgrep
|
- name: Semgrep
|
||||||
run: |
|
run: |
|
||||||
|
set +e
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v ${{ github.workspace }}:/src \
|
-v ${{ gitea.workspace }}:/src \
|
||||||
semgrep/semgrep \
|
semgrep/semgrep \
|
||||||
semgrep scan \
|
semgrep scan \
|
||||||
--config auto \
|
--config auto \
|
||||||
|
--json \
|
||||||
|
--output /src/security-results/semgrep.json \
|
||||||
|
--error \
|
||||||
/src
|
/src
|
||||||
|
echo "$?" > security-results/semgrep.exit
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
# TRIVY FS
|
# TRIVY FS
|
||||||
@@ -45,22 +64,28 @@ jobs:
|
|||||||
|
|
||||||
- name: Trivy Filesystem
|
- name: Trivy Filesystem
|
||||||
run: |
|
run: |
|
||||||
|
set +e
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v ${{ github.workspace }}:/workspace \
|
-v ${{ gitea.workspace }}:/workspace \
|
||||||
aquasec/trivy:latest \
|
aquasec/trivy:latest \
|
||||||
fs \
|
fs \
|
||||||
--scanners vuln,secret,misconfig \
|
--scanners vuln,secret,misconfig \
|
||||||
--severity HIGH,CRITICAL \
|
--severity HIGH,CRITICAL \
|
||||||
|
--format json \
|
||||||
|
--output /workspace/security-results/trivy-fs.json \
|
||||||
--exit-code 1 \
|
--exit-code 1 \
|
||||||
/workspace
|
/workspace
|
||||||
|
echo "$?" > security-results/trivy-fs.exit
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
# DOCKER IMAGE BUILD
|
# DOCKER IMAGE BUILD
|
||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
- name: Build Image
|
- name: Build Image for scan
|
||||||
run: |
|
run: |
|
||||||
docker build -t app:${{ github.sha }} .
|
set +e
|
||||||
|
docker build -t app:${{ gitea.sha }} .
|
||||||
|
echo "$?" > security-results/docker-build.exit
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
# TRIVY IMAGE
|
# TRIVY IMAGE
|
||||||
@@ -68,10 +93,224 @@ jobs:
|
|||||||
|
|
||||||
- name: Trivy Image Scan
|
- name: Trivy Image Scan
|
||||||
run: |
|
run: |
|
||||||
|
set +e
|
||||||
|
if [ "$(cat security-results/docker-build.exit)" != "0" ]; then
|
||||||
|
echo "Skipping image scan because the image build failed."
|
||||||
|
echo "1" > security-results/trivy-image.exit
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v ${{ gitea.workspace }}:/workspace \
|
||||||
aquasec/trivy:latest \
|
aquasec/trivy:latest \
|
||||||
image \
|
image \
|
||||||
--severity HIGH,CRITICAL \
|
--severity HIGH,CRITICAL \
|
||||||
|
--format json \
|
||||||
|
--output /workspace/security-results/trivy-image.json \
|
||||||
--exit-code 1 \
|
--exit-code 1 \
|
||||||
app:${{ github.sha }}
|
app:${{ gitea.sha }}
|
||||||
|
echo "$?" > security-results/trivy-image.exit
|
||||||
|
|
||||||
|
- name: Create Gitea issues for security findings
|
||||||
|
if: always()
|
||||||
|
env:
|
||||||
|
GITEA_API_URL: ${{ gitea.api_url }}
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
GITEA_REF_NAME: ${{ gitea.ref_name }}
|
||||||
|
GITEA_RUN_ID: ${{ gitea.run_id }}
|
||||||
|
GITEA_SERVER_URL: ${{ gitea.server_url }}
|
||||||
|
GITEA_SHA: ${{ gitea.sha }}
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
python3 <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
token = os.environ.get("GITEA_TOKEN", "")
|
||||||
|
api_url = os.environ["GITEA_API_URL"].rstrip("/")
|
||||||
|
repo = os.environ["GITEA_REPOSITORY"]
|
||||||
|
ref_name = os.environ.get("GITEA_REF_NAME", "")
|
||||||
|
sha = os.environ.get("GITEA_SHA", "")
|
||||||
|
run_id = os.environ.get("GITEA_RUN_ID", "")
|
||||||
|
server_url = os.environ.get("GITEA_SERVER_URL", "").rstrip("/")
|
||||||
|
|
||||||
|
if not token:
|
||||||
|
print("GITEA_TOKEN is unavailable; skipping issue creation.")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
owner, name = repo.split("/", 1)
|
||||||
|
issues_url = f"{api_url}/repos/{urllib.parse.quote(owner)}/{urllib.parse.quote(name)}/issues"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"token {token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_json(path, fallback):
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as handle:
|
||||||
|
return json.load(handle)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return fallback
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
print(f"Could not parse {path}: {exc}")
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
def trim(value, limit):
|
||||||
|
value = str(value or "unknown")
|
||||||
|
return value if len(value) <= limit else value[: limit - 3] + "..."
|
||||||
|
|
||||||
|
def find_existing(title):
|
||||||
|
query = urllib.parse.urlencode({"state": "open", "type": "issues", "q": title})
|
||||||
|
request = urllib.request.Request(f"{issues_url}?{query}", headers=headers)
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
issues = json.load(response)
|
||||||
|
return any(issue.get("title") == title for issue in issues)
|
||||||
|
|
||||||
|
def create_issue(title, body):
|
||||||
|
if find_existing(title):
|
||||||
|
print(f"Open issue already exists: {title}")
|
||||||
|
return
|
||||||
|
payload = json.dumps({"title": title, "body": body}).encode("utf-8")
|
||||||
|
request = urllib.request.Request(issues_url, data=payload, headers=headers, method="POST")
|
||||||
|
with urllib.request.urlopen(request, timeout=30) as response:
|
||||||
|
created = json.load(response)
|
||||||
|
print(f"Created issue #{created.get('number')}: {title}")
|
||||||
|
|
||||||
|
def body(scanner, summary, details):
|
||||||
|
run_url = f"{server_url}/{repo}/actions/runs/{run_id}" if server_url and run_id else "Unavailable"
|
||||||
|
lines = [
|
||||||
|
f"Security scanner: `{scanner}`",
|
||||||
|
f"Summary: {summary}",
|
||||||
|
f"Repository: `{repo}`",
|
||||||
|
f"Branch/ref: `{ref_name}`",
|
||||||
|
f"Commit: `{sha}`",
|
||||||
|
f"Action run: {run_url}",
|
||||||
|
"",
|
||||||
|
"Details:",
|
||||||
|
]
|
||||||
|
lines.extend(f"- {key}: {value}" for key, value in details if value not in (None, ""))
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
findings = []
|
||||||
|
|
||||||
|
for finding in load_json("security-results/gitleaks.json", []):
|
||||||
|
rule = finding.get("RuleID") or finding.get("Description") or "secret"
|
||||||
|
file_path = finding.get("File", "unknown")
|
||||||
|
line = finding.get("StartLine", "unknown")
|
||||||
|
title = trim(f"[security][gitleaks] {rule} in {file_path}:{line}", 255)
|
||||||
|
findings.append((
|
||||||
|
title,
|
||||||
|
body("gitleaks", f"{rule} in `{file_path}:{line}`", [
|
||||||
|
("Rule", rule),
|
||||||
|
("Description", finding.get("Description")),
|
||||||
|
("File", file_path),
|
||||||
|
("Line", line),
|
||||||
|
("Fingerprint", finding.get("Fingerprint")),
|
||||||
|
]),
|
||||||
|
))
|
||||||
|
|
||||||
|
semgrep = load_json("security-results/semgrep.json", {})
|
||||||
|
for result in semgrep.get("results", []):
|
||||||
|
extra = result.get("extra", {})
|
||||||
|
start = result.get("start", {})
|
||||||
|
check_id = result.get("check_id", "semgrep finding")
|
||||||
|
file_path = result.get("path", "unknown")
|
||||||
|
line = start.get("line", "unknown")
|
||||||
|
title = trim(f"[security][semgrep] {check_id} in {file_path}:{line}", 255)
|
||||||
|
findings.append((
|
||||||
|
title,
|
||||||
|
body("semgrep", extra.get("message", check_id), [
|
||||||
|
("Check", check_id),
|
||||||
|
("Severity", extra.get("severity")),
|
||||||
|
("File", file_path),
|
||||||
|
("Line", line),
|
||||||
|
("Message", extra.get("message")),
|
||||||
|
]),
|
||||||
|
))
|
||||||
|
|
||||||
|
def add_trivy_findings(path, scanner):
|
||||||
|
report = load_json(path, {})
|
||||||
|
for result in report.get("Results", []):
|
||||||
|
target = result.get("Target", "unknown")
|
||||||
|
for vuln in result.get("Vulnerabilities", []) or []:
|
||||||
|
vuln_id = vuln.get("VulnerabilityID", "vulnerability")
|
||||||
|
package = vuln.get("PkgName", "unknown package")
|
||||||
|
severity = vuln.get("Severity", "UNKNOWN")
|
||||||
|
title = trim(f"[security][{scanner}] {severity} {vuln_id} in {package} on {target}", 255)
|
||||||
|
findings.append((
|
||||||
|
title,
|
||||||
|
body(scanner, vuln.get("Title") or vuln_id, [
|
||||||
|
("Type", "Vulnerability"),
|
||||||
|
("Severity", severity),
|
||||||
|
("Target", target),
|
||||||
|
("Package", package),
|
||||||
|
("Installed version", vuln.get("InstalledVersion")),
|
||||||
|
("Fixed version", vuln.get("FixedVersion")),
|
||||||
|
("Vulnerability ID", vuln_id),
|
||||||
|
("Primary URL", vuln.get("PrimaryURL")),
|
||||||
|
]),
|
||||||
|
))
|
||||||
|
for misconfig in result.get("Misconfigurations", []) or []:
|
||||||
|
misconfig_id = misconfig.get("ID", "misconfiguration")
|
||||||
|
severity = misconfig.get("Severity", "UNKNOWN")
|
||||||
|
title = trim(f"[security][{scanner}] {severity} {misconfig_id} in {target}", 255)
|
||||||
|
findings.append((
|
||||||
|
title,
|
||||||
|
body(scanner, misconfig.get("Title") or misconfig_id, [
|
||||||
|
("Type", "Misconfiguration"),
|
||||||
|
("Severity", severity),
|
||||||
|
("Target", target),
|
||||||
|
("ID", misconfig_id),
|
||||||
|
("Message", misconfig.get("Message")),
|
||||||
|
("Resolution", misconfig.get("Resolution")),
|
||||||
|
]),
|
||||||
|
))
|
||||||
|
for secret in result.get("Secrets", []) or []:
|
||||||
|
rule = secret.get("RuleID", "secret")
|
||||||
|
line = secret.get("StartLine", "unknown")
|
||||||
|
title = trim(f"[security][{scanner}] {rule} in {target}:{line}", 255)
|
||||||
|
findings.append((
|
||||||
|
title,
|
||||||
|
body(scanner, f"{rule} in `{target}:{line}`", [
|
||||||
|
("Type", "Secret"),
|
||||||
|
("Rule", rule),
|
||||||
|
("Category", secret.get("Category")),
|
||||||
|
("Severity", secret.get("Severity")),
|
||||||
|
("Target", target),
|
||||||
|
("Line", line),
|
||||||
|
]),
|
||||||
|
))
|
||||||
|
|
||||||
|
add_trivy_findings("security-results/trivy-fs.json", "trivy-fs")
|
||||||
|
add_trivy_findings("security-results/trivy-image.json", "trivy-image")
|
||||||
|
|
||||||
|
if not findings:
|
||||||
|
print("No security findings found in scanner reports.")
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
for title, issue_body in findings:
|
||||||
|
try:
|
||||||
|
create_issue(title, issue_body)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"Failed to create issue for {title}: {exc}")
|
||||||
|
PY
|
||||||
|
|
||||||
|
- name: Stop on failed security scans
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
failed=0
|
||||||
|
for result in security-results/*.exit; do
|
||||||
|
name="$(basename "$result" .exit)"
|
||||||
|
code="$(cat "$result")"
|
||||||
|
if [ "$code" != "0" ]; then
|
||||||
|
echo "$name failed with exit code $code"
|
||||||
|
failed=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$failed" != "0" ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user