Skip to main content
Non-interactive mode enables Esprit to run in automated environments like CI/CD pipelines, without requiring user input or terminal interaction.

Overview

From esprit/interface/main.py:1092 and esprit/interface/cli.py:82, non-interactive mode bypasses all interactive prompts:
scan_parser.add_argument("-n", "--non-interactive", 
                         action="store_true", 
                         help="Non-interactive mode")

agent_config = {
    "llm_config": llm_config,
    "max_iterations": 300,
    "non_interactive": True,  # Always True in CLI mode
}
All CLI scans run in non-interactive mode by default. The flag is primarily for compatibility and explicitness.

Basic Usage

esprit scan https://example.com --non-interactive
esprit scan https://example.com -n
esprit scan https://staging.example.com \
  --non-interactive \
  --scan-mode quick

CI/CD Integration

GitHub Actions

Integrate Esprit into GitHub Actions workflows:
.github/workflows/security-scan.yml
name: Security Scan

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'  # Weekly on Monday at 2 AM

jobs:
  esprit-scan:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Set up Docker
        uses: docker/setup-docker@v3
      
      - name: Install Esprit
        run: |
          curl -sSL https://install.esprit.dev | bash
          echo "$HOME/.esprit/bin" >> $GITHUB_PATH
      
      - name: Configure LLM Provider
        env:
          ESPRIT_LLM: ${{ secrets.ESPRIT_LLM }}
          LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
        run: |
          esprit provider login esprit <<< "${{ secrets.ESPRIT_TOKEN }}"
      
      - name: Run Security Scan
        run: |
          esprit scan . \
            --non-interactive \
            --scan-mode standard \
            --instruction "Focus on changes in this PR"
      
      - name: Upload Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: esprit-scan-results
          path: esprit_runs/
      
      - name: Check for Critical Vulnerabilities
        run: |
          # Parse results and fail if critical vulns found
          if grep -q '"severity": "critical"' esprit_runs/*/vulnerabilities.json; then
            echo "Critical vulnerabilities found!"
            exit 1
          fi
Post scan results as PR comments:
- name: Comment PR
  if: github.event_name == 'pull_request'
  uses: actions/github-script@v7
  with:
    script: |
      const fs = require('fs');
      const results = fs.readFileSync('esprit_runs/report.md', 'utf8');
      
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: `## Esprit Security Scan Results\n\n${results}`
      });

GitLab CI

.gitlab-ci.yml
stages:
  - security

esprit-scan:
  stage: security
  image: docker:24-dind
  
  services:
    - docker:24-dind
  
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  
  before_script:
    - apk add --no-cache curl bash
    - curl -sSL https://install.esprit.dev | bash
    - export PATH="$HOME/.esprit/bin:$PATH"
  
  script:
    - |
      esprit scan $CI_PROJECT_URL \
        --non-interactive \
        --scan-mode quick \
        --instruction "Focus on MR changes"
  
  artifacts:
    paths:
      - esprit_runs/
    expire_in: 30 days
    when: always
  
  only:
    - merge_requests
    - main

Jenkins Pipeline

Jenkinsfile
pipeline {
    agent any
    
    environment {
        ESPRIT_LLM = credentials('esprit-llm-model')
        LLM_API_KEY = credentials('llm-api-key')
    }
    
    stages {
        stage('Security Scan') {
            steps {
                sh '''
                    curl -sSL https://install.esprit.dev | bash
                    export PATH="$HOME/.esprit/bin:$PATH"
                    
                    esprit scan . \
                        --non-interactive \
                        --scan-mode standard \
                        --instruction "Focus on critical APIs"
                '''
            }
        }
        
        stage('Archive Results') {
            steps {
                archiveArtifacts artifacts: 'esprit_runs/**/*', 
                                 allowEmptyArchive: true
            }
        }
        
        stage('Security Gate') {
            steps {
                script {
                    def criticalVulns = sh(
                        script: '''
                            if [ -f esprit_runs/*/vulnerabilities.json ]; then
                                jq '[.[] | select(.severity=="critical")] | length' \
                                    esprit_runs/*/vulnerabilities.json
                            else
                                echo "0"
                            fi
                        ''',
                        returnStdout: true
                    ).trim().toInteger()
                    
                    if (criticalVulns > 0) {
                        error("Critical vulnerabilities found: ${criticalVulns}")
                    }
                }
            }
        }
    }
    
    post {
        always {
            publishHTML([
                allowMissing: false,
                alwaysLinkToLastBuild: true,
                keepAll: true,
                reportDir: 'esprit_runs',
                reportFiles: '*/report.html',
                reportName: 'Esprit Security Report'
            ])
        }
    }
}

CircleCI

.circleci/config.yml
version: 2.1

Executors:
  esprit-executor:
    docker:
      - image: docker:24
    environment:
      DOCKER_HOST: unix:///var/run/docker.sock

jobs:
  security-scan:
    executor: esprit-executor
    
    steps:
      - checkout
      
      - setup_remote_docker:
          version: 24.0.0
      
      - run:
          name: Install Esprit
          command: |
            apk add --no-cache curl bash
            curl -sSL https://install.esprit.dev | bash
            echo 'export PATH="$HOME/.esprit/bin:$PATH"' >> $BASH_ENV
      
      - run:
          name: Run Security Scan
          command: |
            esprit scan . \
              --non-interactive \
              --scan-mode quick
      
      - store_artifacts:
          path: esprit_runs
          destination: security-scan-results
      
      - run:
          name: Check Results
          command: |
            if grep -q '"severity": "critical"' esprit_runs/*/vulnerabilities.json; then
              echo "Critical vulnerabilities found!"
              exit 1
            fi

workflows:
  version: 2
  security-pipeline:
    jobs:
      - security-scan:
          filters:
            branches:
              only:
                - main
                - develop

Docker Integration

Run Esprit scans in Docker containers:
Dockerfile.esprit-scanner
FROM docker:24-dind

RUN apk add --no-cache \
    curl \
    bash \
    git \
    python3 \
    py3-pip

# Install Esprit
RUN curl -sSL https://install.esprit.dev | bash
ENV PATH="/root/.esprit/bin:${PATH}"

# Copy scan configuration
COPY scan-config.txt /config/instructions.txt

ENTRYPOINT ["esprit"]
CMD ["scan", "--help"]
Run the container:
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v $(pwd):/workspace \
  -e ESPRIT_LLM="openai/gpt-5" \
  -e LLM_API_KEY="$LLM_API_KEY" \
  esprit-scanner:latest \
  scan /workspace \
  --non-interactive \
  --scan-mode quick \
  --instruction-file /config/instructions.txt

Environment Configuration

From esprit/interface/main.py:590, Esprit requires LLM provider configuration. For CI/CD:

Provider Authentication

# Store token as secret
export ESPRIT_TOKEN="your-subscription-token"

# Login non-interactively
echo "$ESPRIT_TOKEN" | esprit provider login esprit

# Set model
export ESPRIT_LLM="esprit/claude-4.5-sonnet"

Pre-Scan Setup

From esprit/interface/main.py:590-730, non-interactive mode auto-selects configuration:
if non_interactive:
    # Auto-select the first available model in non-interactive mode
    selected_model = available_models[0][0]
    os.environ["ESPRIT_LLM"] = selected_model
    Config.save_current()
    current_model = selected_model
    console.print(f"[dim]Auto-selected model: {current_model}[/]")
In non-interactive mode, Esprit automatically selects the first available model if none is configured.

Output Handling

From esprit/interface/cli.py:44, results are saved to esprit_runs/<run-name>/:
results_text = Text()
results_text.append("Output", style="dim")
results_text.append("  ")
results_text.append(f"esprit_runs/{args.run_name}", style="#60a5fa")

Results Directory Structure

esprit_runs/
└── example-com_a3f2/
    ├── vulnerabilities.json      # All vulnerabilities found
    ├── report.md                 # Markdown report
    ├── report.html               # HTML report
    ├── scan_config.json          # Scan configuration
    ├── telemetry.json            # Token usage, timing, etc.
    └── logs/
        ├── agent_001.log
        ├── agent_002.log
        └── main.log

Parsing Results

Process vulnerability data programmatically:
#!/bin/bash
# Parse vulnerabilities and generate report

RESULTS_DIR="esprit_runs/$(ls -t esprit_runs | head -1)"
VULNS_FILE="$RESULTS_DIR/vulnerabilities.json"

if [ ! -f "$VULNS_FILE" ]; then
  echo "No vulnerabilities found"
  exit 0
fi

# Count by severity
CRITICAL=$(jq '[.[] | select(.severity=="critical")] | length' "$VULNS_FILE")
HIGH=$(jq '[.[] | select(.severity=="high")] | length' "$VULNS_FILE")
MEDIUM=$(jq '[.[] | select(.severity=="medium")] | length' "$VULNS_FILE")
LOW=$(jq '[.[] | select(.severity=="low")] | length' "$VULNS_FILE")

echo "Security Scan Results:"
echo "  Critical: $CRITICAL"
echo "  High: $HIGH"
echo "  Medium: $MEDIUM"
echo "  Low: $LOW"

# Extract specific fields
jq -r '.[] | "\(.id): \(.title) [\(.severity)]"' "$VULNS_FILE"

# Generate summary for Slack/email
jq -r '@json' "$VULNS_FILE" > summary.json

Exit Codes

From esprit/interface/cli.py:189-197, Esprit uses standard exit codes:
if isinstance(result, dict) and not result.get("success", True):
    error_msg = result.get("error", "Unknown error")
    error_details = result.get("details")
    console.print()
    console.print(f"[bold red]Penetration test failed:[/] {error_msg}")
    if error_details:
        console.print(f"[dim]{error_details}[/]")
    console.print()
    sys.exit(1)
  • 0 - Scan completed successfully
  • 1 - Scan failed (runtime error, configuration issue)
Esprit returns exit code 0 even if vulnerabilities are found. You must parse the results to implement quality gates.

Best Practices

Never hardcode credentials in pipeline files:
# ✅ Good - Use secrets
env:
  LLM_API_KEY: ${{ secrets.LLM_API_KEY }}

# ❌ Bad - Hardcoded credentials
env:
  LLM_API_KEY: "sk-1234567890"
  • Quick mode for every commit/PR (fast feedback)
  • Standard mode for nightly builds or pre-release
  • Deep mode for weekly comprehensive scans
Define clear security standards:
# Fail on any critical
# Warn on >5 high severity
# Allow unlimited medium/low
Always store scan results as artifacts:
  • Trend analysis over time
  • Compliance evidence
  • Historical comparison
  • Debugging failed scans
Provide PR/commit context:
esprit scan . \
  --instruction "Focus on changes in PR #${{ github.event.pull_request.number }}"
Track scan duration and costs:
# Log telemetry data
cat esprit_runs/*/telemetry.json >> scan_metrics.jsonl

Troubleshooting

If Esprit can’t connect to Docker in CI:
# Ensure Docker socket is mounted
volumes:
  - /var/run/docker.sock:/var/run/docker.sock

# Or use Docker-in-Docker
services:
  - docker:dind

environment:
  DOCKER_HOST: tcp://docker:2375

Next Steps

Scanning Targets

Learn how to scan different target types

Scan Modes

Choose between quick, standard, and deep modes

Build docs developers (and LLMs) love