GitLab CI Integration Guide¶
Configure continuous FinOps scanning, MR comments, and security gating in .gitlab-ci.yml.
Quick Start (5-Minute Setup)¶
Prerequisites¶
- GitLab account with admin access
- GitLab CI/CD enabled on project
- CloudSlash API key (from dashboard)
- (Optional) GitLab Personal Access Token
Step 1: Install CloudSlash¶
Add CloudStencil to your .gitlab-ci.yml:
# .gitlab-ci.yml
stages:
- infrastructure
- metrics
cloudslash-scan:
stage: metrics
image: cloudslash/scan:latest
script:
- cloudslash scan --config cloudslash.yml
artifacts:
reports:
cloudslash: cloudslash-report.json
only:
- branches
- merge_requests
- tags
Step 2: Add Variables¶
Go to your project → Settings → CI/CD → Variables:
- Add
CLOUDSLASH_API_KEYwith your CloudSlash API key - (Optional) Add
GITLAB_TOKENfor API access to public repositories
Step 3: Configure CloudSlash¶
Create cloudslash.yml in your repository root:
version: 1.0
accounts:
- provider: aws
name: production
accounts:
- id: "123456789012"
role_arn: "arn:aws:iam::123456789012:role/CloudSlashAccess"
security:
compliance:
enabled: true
benchmark: cis
notifications:
slack:
webhook_url: "${{ GITLAB_SLACK_WEBHOOK }}"
Done! Prepended MRs will automatically run scans.
Full Configuration Reference¶
Common Workflow Patterns¶
Pattern 1: PR/MR Cost Scanning¶
Prevent costly changes before merging:
# .gitlab-ci.yml
stages:
- infrastructure
- cost-analysis
cost-scan:
stage: cost-analysis
image: cloudslash/scan:latest
script:
- cloudslash scan --config cloudslash.yml
# Run on each MR branch
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: always
# Add MR notes
artifacts:
reports:
cloudslash: cost-analysis.json
expire_in: 1 week
paths:
- cost-analysis.json
- cost-analysis.html
allow_failure: true
# Merge request comment with summary
after_script:
- |
# Generate PR comment
SCRIPT_DIR="$(dirname "$0")"
python3 $SCRIPT_DIR/add_pr_comment.py
creates script: .gitlab-ci/scripts/add_pr_comment.py
#!/usr/bin/env python3
import json
import sys
# Read the report
with open('cost-analysis.json', 'r') as f:
data = json.load(f)
# Generate comment
title = f"📊 Cost Analysis for {data['summary']['repository']}"
comment = f"""
### Cost Analysis
**Pipeline**: {data['summary']['pipeline_id']}
**Author**: {data['summary']['author']}
**Duration**: {data['summary']['duration']}s
**Total Estimated Monthly Cost**: $**{data['summary']['estimated_cost']}**
### Findings:
"""
for finding in data['findings']:
comment += f"- **{finding['severity']}**: {finding['resource']} - $**{finding['cost']}/month**\n"
comment += f"""
[View full report]({data['summary']['report_url']})
"""
# Send to MR comment
print(comment)
Pattern 2: Scheduled Drift Detection¶
Monitor production infrastructure changes continuously:
# .gitlab-ci.yml
stages:
- maintenance
- drift-detection
drift-detection:
stage: maintenance
image: cloudslash/scan:latest
script:
- |
SCAN_ID="drift-$(date +%Y%m%d-%H%M%S)"
echo "Running drift detection with scan-id: $SCAN_ID"
cloudslash scan --config cloudslash.yml --mode scheduled --scan-id "$SCAN_ID"
rules:
# Run daily at 2 AM UTC (save resources in off-peak hours)
- if: '$CI_SCHEDULED == "true"'
when: always
# Allow manual trigger from UI
- if: '$GITLAB_USER_LOGIN == "manual-trigger"'
when: manual
artifacts:
reports:
cloudslash: drift-detection-report.json
paths:
- drift-detection-report.json
- drift-detection-report.html
expire_in: 30 days
after_script:
- |
# Create issue on MR if drift detected
SCRIPT_DIR="$(dirname "$0")"
python3 $SCRIPT_DIR/check-drift.py --scan-id "$SCAN_ID"
creates script: .gitlab-ci/scripts/check-drift.py
#!/usr/bin/env python3
import json
import subprocess
import sys
from datetime import datetime
def check_drift(scan_id):
# Fetch report from CloudSlash
result = subprocess.run(
['cloudslash', 'report', '--id', scan_id, '--format', 'json'],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"Error fetching report: {result.stderr}")
return
data = json.loads(result.stdout)
# Check for drift
if data.get('drift', {}).get('change_count', 0) > 0:
title = f"⚠️ Infrastructure Drift Detected (Scan: {scan_id})"
body = f"""## 🚨 Infrastructure Drift Detected
**Scan ID**: {scan_id}
**Time**: {datetime.utcnow().isoformat()}
**Drift Changes**: {data['drift']['change_count']}
**Potential Cost Impact**: $**{data['drift']['cost_impact']}**
### Modified Resources:
"""
for change in data['drift']['changes']:
body += f"- **{change['resource']}** ({change['provider']}): {change['change_type']}\n"
body += f"""
[View full report](#report-viewer)
"""
print("DRIFT_DETECTED")
print(f"ISSUE_TITLE={title}")
print(f"ISSUE_BODY={body}")
else:
print("NO_DRIFT")
print("✅ Infrastructure is in sync with your IaC.")
def main():
scan_id = sys.argv[1] if len(sys.argv) > 1 else None
if scan_id:
check_drift(scan_id)
else:
print("Usage: python3 check-drift.py --scan-id <scan_id>")
if __name__ == '__main__':
main()
Pattern 3: Deployment Gates¶
Block deployments that don't meet security requirements:
# .gitlab-ci.yml
stages:
- build
- security
- deploy
deployment-security-check:
stage: security
image: cloudslash/scan:latest
script:
- cloudslash scan --config cloudslash.yml --mode deployment-gate
needs:
- build
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual # Require manual approval
allow_failure: false
# Send results to GitLab merge request
artifacts:
reports:
cloudslash: deployment-gate-report.json
after_script:
- |
if [ "$CLOUDSLASH_PASSED" != "true" ]; then
echo "❌ Security checks failed"
exit 1
fi
echo "✅ Security checks passed"
Note: This would require a merged codebase, so it's typically used after merge:
# .gitlab-ci.yml (in protected branch)
stages:
- deploy
production-deployment-gate:
stage: deploy
image: cloudslash/scan:latest
script:
- cloudslash scan --config cloudslash.yml --mode deployment-gate --fail-on-warnings
when: manual # Require approval
only:
- main
Pattern 4: Monorepo Setup¶
Centralized scanning for multiple repositories:
# .gitlab-ci.yml (root monorepo)
stages:
- docs
- infra-scan
subrepo-scan:
stage: infra-scan
image: cloudslash/scan:latest
script:
# Scan frontend-api
- |
cd frontend-api
cloudslash scan --config cloudslash.yml
# Scan backend-api
- |
cd backend-api
cloudslash scan --config cloudslash.yml
# Scan infrastructure
- |
cd infrastructure
cloudslash scan --config cloudslash.yml
artifacts:
paths:
- frontend-api/cloudslash-results.json
- backend-api/cloudslash-results.json
- infrastructure/cloudslash-results.json
expire_in: 30 days
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
after_script:
- |
# Aggregate results and create comment
SCRIPT_DIR="$(dirname "$0")"
python3 $SCRIPT_DIR/aggregate-results.py
# .gitlab-ci.yml (in each subro repo)
cloudslash-scan:
stage: infra-scan
image: cloudslash/scan:latest
script:
- cloudslash scan --config cloudslash.yml
artifacts:
reports:
cloudslash: cloudslash-results.json
Aggregate script: .gitlab-ci/scripts/aggregate-results.py
#!/usr/bin/env python3
import json
import sys
def aggregate_results():
results = {
'frontend': None,
'backend': None,
'infrastructure': None
}
# Read each subrepo result
try:
with open('frontend-api/cloudslash-results.json') as f:
results['frontend'] = json.load(f)
except FileNotFoundError:
pass
try:
with open('backend-api/cloudslash-results.json') as f:
results['backend'] = json.load(f)
except FileNotFoundError:
pass
try:
with open('infrastructure/cloudslash-results.json') as f:
results['infrastructure'] = json.load(f)
except FileNotFoundError:
pass
# Generate aggregate comment
title = "📊 Monorepo Infrastructure Scan"
comment = f"""### Monorepo Infrastructure Scan
**Backend**: {len(results['backend'].get('findings', [])) if results['backend'] else 0} findings
**Frontend**: {len(results['frontend'].get('findings', [])) if results['frontend'] else 0} findings
**Infrastructure**: {len(results['infrastructure'].get('findings', [])) if results['infrastructure'] else 0} findings
[View individual reports]
"""
print(title)
print(comment)
if __name__ == '__main__':
aggregate_results()
Pattern 5: Multi-Cloud Setup¶
Unified scanning across AWS, Azure, and GCP:
# .gitlab-ci.yml
stages:
- infrastructure
- multi-cloud-check
aws-monitoring:
stage: infrastructure
image: cloudslash/scan:latest
variables:
CLOUDSLASH_CONFIG: aws-config.yml
script:
- cloudslash scan --config "$CLOUDSLASH_CONFIG"
artifacts:
reports:
cloudslash: aws-monitoring.json
azure-monitoring:
stage: infrastructure
image: cloudslash/scan:latest
variables:
CLOUDSLASH_CONFIG: azure-config.yml
script:
- cloudslash scan --config "$CLOUDSLASH_CONFIG"
artifacts:
reports:
cloudslash: azure-monitoring.json
gcp-monitoring:
stage: infrastructure
image: cloudslash/scan:latest
variables:
CLOUDSLASH_CONFIG: gcp-config.yml
script:
- cloudslash scan --config "$CLOUDSLASH_CONFIG"
artifacts:
reports:
cloudslash: gcp-monitoring.json
aggregate-multi-cloud:
stage: multi-cloud-check
image: python:3.9
script:
- >
python3 << 'EOF'
# Read all three cloud reports
with open('aws-monitoring.json') as f:
aws_data = json.load(f)
with open('azure-monitoring.json') as f:
azure_data = json.load(f)
with open('gcp-monitoring.json') as f:
gcp_data = json.load(f)
# Aggregate findings
all_findings = (aws_data.get('findings', []) +
azure_data.get('findings', []) +
gcp_data.get('findings', []))
total_cost = (aws_data.get('summary', {}).get('estimated_cost', 0) +
azure_data.get('summary', {}).get('estimated_cost', 0) +
gcp_data.get('summary', {}).get('estimated_cost', 0))
print(f"Total Multi-Cloud Findings: {len(all_findings)}")
print(f"Estimated Monthly Cost: $**{total_cost}**")
print(f"AWS Findings: {len(aws_data['findings'])}")
print(f"Azure Findings: {len(azure_data['findings'])}")
print(f"GCP Findings: {len(gcp_data['findings'])}")
EOF
dependencies:
- aws-monitoring
- azure-monitoring
- gcp-monitoring
# Example configs
# aws-config.yml
version: 1.0
accounts:
- provider: aws
name: production
accounts:
- id: "111111111111"
role_arn: "arn:aws:iam::111111111111:role/CloudSlashRole"
# azure-config.yml
version: 1.0
accounts:
- provider: azure
name: production
subscriptions:
- id: "11111111-1111-1111-1111-111111111111"
# gcp-config.yml
version: 1.0
accounts:
- provider: gcp
name: production
projects:
- id: "my-project-123"
Security Best Practices¶
1. Use Protected Variables¶
Store credentials in project-level variables:
# ✅ DO THIS: Use protected variables
variables:
CLOUDSLASH_API_KEY: "sk-api-key"
AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY
AWS_SECRET_ACCESS_KEY: $AWS_SECRET
2. Mask Sensitive Variables¶
In .gitlab-ci.yml, set roles to masked:
drift-detection:
mask:
- CLOUDSLASH_API_KEY
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
script:
- cloudslash scan --config cloudslash.yml
3. Run in Stages¶
Use proper CI/CD pipeline stages:
stages:
- build
- test
- infrastructure-scan
- deploy
security-scan:
stage: infrastructure-scan
script:
- cloudslash scan --config security-config.yml
dependencies: [test-results]
deploy:
stage: deploy
script:
- echo "Deploy..."
dependencies: [security-scan]
only:
- main
4. Use Rules Instead of If¶
For conditional execution in pipelines:
tag-based-scan:
stage: infrastructure-scan
script:
- cloudslash scan --config config.yml
rules:
- if: $CI_COMMIT_TAG
when: always
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
- when: never # Don't run on other branches
5. Limit Runner Permissions¶
Use generic runners with limited permissions:
# .gitlab-ci.yml
security-scan:
image: cloudslash/scan:latest
services: [] # Disable service containers
tags: ["generic", "security-scan"]
script:
- cloudslash scan --config cloudslash.yml
Multi-Account Setup¶
Using OAuth 2.0¶
For enterprise GitLab setups:
# .gitlab-ci.yml
oauth-scan:
image: cloudslash/scan:latest
variables:
CLOUDSLASH_OAUTH_TOKEN: $CLOUDSLASH_OAUTH_TOKEN
script:
- cloudslash scan --config cloudslash.yml --auth-type oauth
Using SSH Keys¶
For SSH-based authentication:
ssh-scan:
image: cloudslash/scan:latest
before_script:
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
script:
- cloudslash scan --config cloudslash.yml --auth-type ssh
Using CI Job Variables¶
Environment-specific configurations:
stages:
- test
- production-scan
dev-scan:
stage: test
variables:
CLOUDSLASH_MODE: pr
CLOUDSLASH_ACCOUNTS: development-accounts
image: cloudslash/scan:latest
script:
- cloudslash scan --config .gitlab-ci/config.yml
prod-scan:
stage: production-scan
variables:
CLOUDSLASH_MODE: deployment-gate
CLOUDSLASH_ACCOUNTS: production-accounts
image: cloudslash/scan:latest
script:
- cloudslash scan --config .gitlab-ci/config.yml
when: manual
Notification Integration¶
Slack Integration¶
slack-notification:
image: python:3.9
before_script:
- pip install requests
script:
- >
python3 << 'EOF'
import requests
import json
import os
with open('cloudslash-report.json') as f:
report = json.load(f)
webhook_url = os.getenv('SLACK_WEBHOOK_URL')
if not webhook_url:
print("No webhook URL configured")
exit(0)
message = {
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"📊 CloudSlash Scan: {report['summary']['status']}"
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Result:*\n{report['summary']['status']}"
},
{
"type": "mrkdwn",
"text": f"*Findings:*\n{report['summary']['findings']}"
},
{
"type": "mrkdwn",
"text": f"*Duration:*\n{report['summary']['duration']}s"
}
]
}
]
}
response = requests.post(webhook_url, json=message)
print(f"Slack notification sent: {response.status_code}")
EOF
dependencies:
- cloudslash-scan
Email Notifications¶
email-report:
image: composer:latest
script:
- composer install
- php send_email.php --report cloudslash-report.html
after_script:
- cat send_email.log
allow_failure: true
send_email.php:
<?php
require 'vendor/autoload.php';
$email = $_SERVER['CI_REGISTRY_USER'] . '@yourcompany.com';
$subject = 'CloudSlash Report';
$body = file_get_contents('cloudslash-report.html');
// Send email (using SwiftMailer or PHPMailer)
GitLab Merge Request Comments¶
merge-request-comment:
image: cloudslash/scan:latest
script:
- cloudslash scan --config cloudslash.yml --mode pr
artifacts:
reports:
cloudslash: pr-report.json
after_script:
- >
if [ -f "pr-report.json" ]; then
TOKEN="${CI_JOB_TOKEN}"
PROJECT_ID="${CI_PROJECT_ID}"
MR_IID="${CI_MERGE_REQUEST_IID}"
URL="https://gitlab.com/api/v4/projects/${PROJECT_ID}/merge_requests/${MR_IID}/notes"
# Read the comment file
COMMENT_FILE="pr-comment.md"
COMMENT=$(cat "$COMMENT_FILE")
# POST to MR
RESPONSE=$(curl --header "PRIVATE-TOKEN: $TOKEN" \
--header "Content-Type: application/json" \
--data '{"body": "'"${COMMENT}"'"}' \
"$URL")
echo "MR comment posted: $RESPONSE"
fi
Troubleshooting¶
Common Issues¶
Issue 1: Permissions Denied¶
Symptom: "Permission denied: /path/to/report"
Solution:
# Fix permission issues
infra-scan:
before_script:
- chmod +x cloudslash
- chmod -R 755 .
script:
- cloudslash scan --config cloudslash.yml
Issue 2: SSH Auth Errors¶
Symptom: "SSH authentication failed"
Solution:
before_script:
- eval $(ssh-agent -s)
- ssh-add -l
- git config --global user.name "GitLab Runner"
- git config --global user.email "runner@gitlab.com"
Issue 3: Image Not Pulling¶
Symptom: "Error: image cloudslash/scan:latest not found"
Solution:
Issue 4: caching fails¶
Symptom: "Cannot compile/play with this language version"
Solution:
service-docker:
image: docker:dind
services:
- docker:dind
script:
- docker pull cloudslash/scan:latest
NOTE: Linux image caching may have different behavior than macOS. Verify results are hash-consistent and stable across providers.
Issue 5: Swiftpackage Runner Missing¶
Symptom: No swift's CI integration or Swift installed, preventing swift-specific sample code from running. Use macOS runners or configure s.version for consumers, or skip or remove sample Swift workflows.
Workaround in docs: Document that macOS runners required, pass s.version if using swift Example from CI/CD: https://gitlab.com/gl_versions/samples/-/blob/main/.swiftlint_omitted.yml
Feature Comparison Matrix¶
GitLab CI vs Other Platforms¶
| Feature | GitHub Actions | GitLab CI | Azure DevOps | CircleCI | Jenkins |
|---|---|---|---|---|---|
| Setup Time (min) | ⚡ 2-3 | ⚡ 2-3 | ⭐ 3-5 | ⚡ 2-4 | ⭐ 5-10 |
| PR/MR Comments | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Free Plan | ⭐⭐⭐⭐⭐ (unlimited) | ⭐⭐⭐⭐⭐ (unlimited) | ⭐⭐⭐⭐ (limited) | ⭐⭐⭐ (limited) | ⭐⭐ (limited) |
| Scheduled Jobs | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Monorepo Support | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Multi-Cloud Support | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Docker Execution | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Custom Actions | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Cost Build Minutes | ⚡ Free (public) | ⚡ Free (public) | ⭐ Free (small) | ⭐ Limited | ⚡ Free (self-hosted) |
| Integrations | ⭐⭐⭐⭐⭐ (AWS, Azure, GCP) | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Best Overall: GitLab CI (First-class PR/MR support, unlimited free pipelines)
Click for detailed feature descriptions
### GitLab CI Strengths - ✅ **Native PR/MR integration**: Seamless integration with GitLab UI - ✅ **Variable management**: Built-in variable masking and group-level secrets - ✅ **MR comments**: Automatic merge request comments on scan failure - ✅ **Protected branches**: Built-in protection and approval workflows - ✅ **Free for public and small projects**: No build minute limits ### GitLab CI Weaknesses - ❌ ❌ Limited Docker-in-Docker (DIN/DinD) support compared to GitHub Actions - ❌ Matrix jobs configuration can be verbose - ❌ No self-hosted runners for free (requires premium) - ❌ Stage dependency ordering must be careful (fail-silence risk)Common Patterns¶
Pattern 1: Conditional Scan Execution¶
Run scans based on changes:
dev-scan:
stage: test
script:
- cloudslash scan --config cloudslash.yml
rules:
# Only run when config files change
- changes:
- **/*config*.yaml
- **/*config*.yml
- infra/*
when: always
# OR shallow check for changed resources
check-changed-resources:
stage: test
script:
- git diff --name-only main > changes.txt
- cloudslash scan --config cloudslash.yml --changed-file changes.txt
when: manual
Pattern 2: Scan with Different Rules Per Branch¶
Different rules for main vs dev:
# For main/production branches
prod-scan:
stage: security
image: cloudslash/scan:latest
script:
- cloudslash scan --config config.yml --mode deployment-gate
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
when: always
only:
- main
# For dev branches
dev-scan:
stage: test
image: cloudslash/scan:latest
script:
- cloudslash scan --config config.yml --mode pr
rules:
- if: '$CI_COMMIT_BRANCH == "develop" || $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == "develop"'
when: always
- if: '$CI_COMMIT_BRANCH == "main"'
when: never
Pattern 3: Containerized Scanner¶
Build custom Docker image with tools:
build-scanner:
stage: build
image: docker:19
services:
- docker:dind
script:
- docker build -t cloudslash-scanner:latest .
artifacts:
paths:
- cloudslash-scanner:latest
infra-scan:
stage: test
image: cloudslash-scanner:latest
script:
- cloudslash scan --config cloudslash.yml
Best Practices¶
1. Use Artifacts for Reports¶
Store scan results for downstream jobs:
infra-scan:
script:
- cloudslash scan --config cloudslash.yml
artifacts:
reports:
cloudslash: report.json
paths:
- report.json
- report.html
expire_in: 30 days
2. Cache Dependencies¶
Cache CloudSlash and dependencies:
infra-scan:
image: cloudslash/scan:latest
before_script:
- export CLOUDSLASH_CONFIG=$PWD/cloudslash.yml
- export CACHED_ACCESS_TOKEN_PARTS=cloudslash-shared/lib/dep-cache
- |
# Build cache keys from config and deps
CONFIG_HASH=$(md5sum cloudslash.yml | cut -d' ' -f1)
DEP_HASH=$(md5sum -r cloudslash-shared/lib/dep-cache | md5sum | cut -d' ' -f1)
CACHE_KEY="cloudslash:$CONFIG_HASH:$DEP_HASH"
- |
docker ps -q
docker pull ci:cloudslash:$CONFIG_HASH:$DEP_HASH || docker build -t ci:cloudslash:$CONFIG_HASH:$DEP_HASH .
- CI_RUNTIME=$CI_RUNTIME
- CLOUDSLASH_SDK_RUNTIME=$CLOUDSLASH_SDK_RUNTIME
Note: Ensure hashes are stable and reproduceable. Test manually in .gitlab-ci.yml to confirm.
3. Use Tags for Runner Selection¶
Run scans on appropriate runners:
FAQ¶
Can I use CloudSlash on forks?
Yes, fork branches can use the variable environment passed by downstream CI, but the variable is not the same as the CI environment variable; the downstream CI is already set up for fork usage. If set, it is the same var as the runner env. When running on a forker-provided runner, the path to used cloudslash-runner (US/Linux) cross-platform images is /; fork uses that target; use the default path.How do I set up scheduled scans?
Use the CI scheduler: Go to: CI/CD → Schedules → New schedule.Can I cache the scanner container?
Yes, use the cached image for consistent scans:Documentation Version: 2.0.0 Last Updated: September 10, 2026 Related: CI/CD Integration Guide | Best Practices