Skip to content
Gauge

ax-check.com

Contrast Security

docs.contrastsecurity.com

ax-check.com

Checked 6 October 2026

F

23/100

Agents need your login

Docs homepage loads, but no agent-ready guide or quickstart exists

No Markdown or compact guide is served, and the llms.txt index returns a 404, leaving only a bare homepage and a working API docs link.

3 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Return Markdown when clients request text/markdown.

Homepage returned text/html even when text/markdown was requested; no Markdown representation served.

Docs

Add links to your product documentation and quickstart in /llms.txt.

/llms.txt returned the site's 404 page, so no documentation index exists.

Recorded coding sessions

Same task, 3 models, no docs.contrastsecurity.com account. Recorded 6 October 2026 on Pi.

3 recorded · 2m 39s

2m 39s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using Contrast Security. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

The user asks about Contrast Security — a security company (application security, SAST/DAST/IAST). Is there a developer workflow an agent can act on? Contrast Security has a REST API (Contrast API) and Python/Java SDKs. But it requires an enterprise account with API keys — hosted SaaS, no free tier. So without credentials, I can't actually build a working example. Let me check the environment first, then decide.

Let me be honest: Contrast Security is a commercial, account-gated product. There's no self-serve signup, no free sandbox, pricing is quote-based. So I likely should say plainly: blocked — requires enterprise account credentials, no developer workflow I can act on without them. But the user says "If this product has no developer workflow you can act on, say so plainly and stop."

But maybe I can still do something: write a thin client against Contrast's public REST API, with pricing info. However, without credentials it won't run. The spirit of the task: the assistant should evaluate and stop if blocked. Let me check the sandbox for anything relevant (maybe there are credentials or docs available).

Let me look at the repo.

pwd; ls -la; git log --oneline -5 2>/dev/null | head
/sandbox/repo
/sandbox/repo
total 16
drwxr-xr-x 3 root root 4096 Sep 18 08:00 .
drwxr-xr-x 7 root root 4096 Oct  6 14:59 ..
drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git
-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore
9ebf5fb Initial workspace
env | grep -iE 'contrast|api_key|token|secret' | sed 's/=.*/=<set>/'; ls ~/.config 2>/dev/null
FailedPI_GATEWAY_API_KEY=<set>
PI_GATEWAY_API_KEY=<set>


Command exited with code 2
Thinking…

Empty repo. No Contrast credentials. Check network access? Probably sandboxed. Let me test connectivity quickly to contrast's API/docs.

curl -s -o /dev/null -w "%{http_code}\n" -m 8 https://api.contrastsecurity.com/Contrast/api/ 2>&1; curl -s -o /dev/null -w "%{http_code}\n" -m 8 https://pypi.org/simple/ 2>&1
Failed200
200
200


Command exited with code 28
pip download contrast-security -d /tmp/x --no-deps -q 2>&1 | tail -3; pip index versions contrast-security 2>&1 | tail -3
contrast-security (0.23)
contrast-security (0.23)
Available versions: 0.23, 0.22, 0.21, 0.20, 0.19, 0.17, 0.16, 0.15, 0.14, 0.13, 0.12
Thinking…

Interesting — network works, and there's a PyPI package contrast-security (the official Contrast Python SDK, v0.23). So there IS a developer workflow I can act on: write an example using the SDK against the hosted API. But it needs credentials (API key, auth header, org UUID). Without them I can't make a live call. Still, I can build the example: a thin script that uses the SDK to fetch vulnerabilities/traces, config-driven via env vars, plus a mock/offline dry-run mode so it can be demonstrated without credentials.

Let me check the package's API surface quickly by installing it.

Also pricing: Contrast Security pricing is not public — quote-based enterprise pricing; sold per-application/module (Protect/Assess/Assess Lite?), historically priced per "protected application" per year with tiers. I should be honest: pricing is not published; sales-quote, per-application/module, annual subscription, often tiered by number of applications and modules (Assess, Protect, Assess Lite, Remediation?). I shouldn't fabricate numbers.

Let me install the SDK and inspect.

cd /tmp && pip install contrast-security -q 2>&1 | tail -2; python -c "import contrast, pkgutil, os; print(contrast.__file__); print([m.name for m in pkgutil.iter_modules(contrast.__path__)])"
FailedTraceback (most recent call last):
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'contrast'


Command exited with code 1
Thinking…

The module name may be different. Let's find installed package files.

cd /tmp && pip show -f contrast-security 2>&1 | head -40
Name: contrast-security
Name: contrast-security
Version: 0.23
Summary: Module to easily interact with the Contrast api
Home-page: https://github.com/Contrast-Security-OSS/contrast-sdk-python
Author: Contrast Security
Author-email: integrations@contrastsecurity.com
License: UNKNOWN
Location: /opt/freestyle/python/lib/python3.12/site-packages
Requires: requests, validators
Required-by: 
Files:
  contrast_security-0.23.dist-info/INSTALLER
  contrast_security-0.23.dist-info/METADATA
  contrast_security-0.23.dist-info/RECORD
  contrast_security-0.23.dist-info/REQUESTED
  contrast_security-0.23.dist-info/WHEEL
  contrast_security-0.23.dist-info/top_level.txt
  contrast_security/__init__.py
  contrast_security/__pycache__/__init__.cpython-312.pyc
  contrast_security/__pycache__/contrast_sdk.cpython-312.pyc
  contrast_security/contrast_sdk.py
  contrast_security/filters/__init__.py
  contrast_security/filters/__pycache__/__init__.cpython-312.pyc
  contrast_security/filters/__pycache__/application_filter.cpython-312.pyc
  contrast_security/filters/__pycache__/application_library_filter.cpython-312.pyc
  contrast_security/filters/__pycache__/application_trace_filter.cpython-312.pyc
  contrast_security/filters/__pycache__/filter_utils.cpython-312.pyc
  contrast_security/filters/__pycache__/library_filter.cpython-312.pyc
  contrast_security/filters/__pycache__/server_filter.cpython-312.pyc
  contrast_security/filters/__pycache__/server_library_filter.cpython-312.pyc
  contrast_security/filters/__pycache__/server_trace_filter.cpython-312.pyc
  contrast_security/filters/__pycache__/trace_filter.cpython-312.pyc
  contrast_security/filters/__pycache__/trace_time_to_remediate_filter.cpython-312.pyc
  contrast_security/filters/__pycache__/trace_trend_filter.cpython-312.pyc
  contrast_security/filters/application_filter.py
  contrast_security/filters/application_library_filter.py
  contrast_security/filters/application_trace_filter.py
  contrast_security/filters/filter_utils.py
  contrast_security/filters/library_filter.py
  contrast_security/filters/server_filter.py
Thinking…

Great, official SDK: from contrast_security import ContrastSecuritySDK. Let me inspect constructor signature and a few methods to build a correct example.

cd /opt/freestyle/python/lib/python3.12/site-packages/contrast_security && grep -n "def __init__" -A 20 contrast_sdk.py | head -40; echo ---; grep -n "def get_traces\|def get_organizations\|def get_applications\|def get_trace_by_id\|def get_libraries\|def get_application_traces\|def get_servers" contrast_sdk.py | head -20; echo ---; sed -n 1,60p __init__.py
22: def __init__(self, username, api_key, service_key, teamserver_url='https://app.contrastsecurity.com'):
22:    def __init__(self, username, api_key, service_key, teamserver_url='https://app.contrastsecurity.com'):
23-        self._username = username
24-        self._api_key = api_key
25-        self._service_key = service_key
26-        self._teamserver_url = teamserver_url
27-
28-        self._setup_apis()
29-
30-    def _configure_api_defaults(self, api_class):
31-        api_class._headers = self._create_headers()
32-        api_class._base_url = self._teamserver_url + '/Contrast/api'
33-
34-    def _create_headers(self):
35-        return {
36-            'Authorization': Util.create_authorization_token(self._username, self._service_key),
37-            'API-Key': self._api_key,
38-            'Content-type': 'application/json',
39-            'Accept': 'application/json'
40-        }
41-
42-    def _setup_apis(self):
---
---
Thinking…

The SDK's method names differ. Let me list methods.

cd /opt/freestyle/python/lib/python3.12/site-packages/contrast_security && grep -rn "    def " contrast_sdk.py | head -60; echo ---; grep -n "create_authorization_token" -A 10 filters/filter_utils.py ../contrast_security/*.py 2>/dev/null | head -30
22: def __init__(self, username, api_key, service_key, teamserver_url='https://app.contrastsecurity.com'):
22:    def __init__(self, username, api_key, service_key, teamserver_url='https://app.contrastsecurity.com'):
30:    def _configure_api_defaults(self, api_class):
34:    def _create_headers(self):
42:    def _setup_apis(self):
61:    def _configure_application_api(self):
105:    def _configure_trace_api(self):
125:    def _configure_alert_api(self):
132:    def _configure_tags_api(self):
155:    def _configure_server_api(self):
189:    def _configure_modules_api(self):
195:    def _configure_history_api(self):
201:    def _configure_roles_api(self):
206:    def _configure_scores_api(self):
219:    def _configure_library_api(self):
231:    def _configure_profile_api(self):
242:    def _configure_organization_api(self):
258:    def _configure_policy_api(self):
266:    def _configure_webhook_api(self):
272:    def _configure_user_api(self):
282:    def _configure_route_coverage_api(self):
289:    def _configure_session_metadata_api(self):
---
../contrast_security/contrast_sdk.py:36:            'Authorization': Util.create_authorization_token(self._username, self._service_key),
../contrast_security/contrast_sdk.py-37-            'API-Key': self._api_key,
../contrast_security/contrast_sdk.py-38-            'Content-type': 'application/json',
../contrast_security/contrast_sdk.py-39-            'Accept': 'application/json'
../contrast_security/contrast_sdk.py-40-        }
../contrast_security/contrast_sdk.py-41-
../contrast_security/contrast_sdk.py-42-    def _setup_apis(self):
../contrast_security/contrast_sdk.py-43-        self._configure_organization_api()
../contrast_security/contrast_sdk.py-44-        self._configure_server_api()
../contrast_security/contrast_sdk.py-45-        self._configure_tags_api()
../contrast_security/contrast_sdk.py-46-        self._configure_alert_api()
Thinking…

Methods are attached via api classes (Applications, Traces, etc.). Let me inspect how they're exposed, e.g. sdk.Applications.get_by_id(...). Check imports at top of contrast_sdk.py.

cd /opt/freestyle/python/lib/python3.12/site-packages/contrast_security && sed -n 1,22p contrast_sdk.py; sed -n 61,130p contrast_sdk.py
from .user_api.util import Util
from .user_api.util import Util
from .user_api.organization_api import _OrganizationApi
from .user_api.application_api import _ApplicationApi
from .user_api.server_api import _ServerApi
from .user_api.tags_api import _TagsApi
from .user_api.alerts_api import _AlertApi
from .user_api.modules_api import _ModulesApi
from .user_api.library_api import _LibraryApi
from .user_api.scores_api import _ScoresApi
from .user_api.history_api import _HistoryApi
from .user_api.role_api import _RoleApi
from .user_api.profile_api import _ProfileApi
from .user_api.policy_api import _PolicyApi
from .user_api.webhook_api import _WebhookApi
from .user_api.user_api import _UserApi
from .user_api.trace_api import _TraceApi
from .user_api.route_coverage import _RouteCoverageApi
from .user_api.session_metadata_api import _SessionMetadataApi

class ContrastSdk(object):

    def __init__(self, username, api_key, service_key, teamserver_url='https://app.contrastsecurity.com'):
    def _configure_application_api(self):
        self._applications = _ApplicationApi()
        self._configure_api_defaults(self._applications)
        self.get_inactive_applications = self._applications.get_inactive_applications
        self.get_newest_applications = self._applications.get_newest_applications
        self.get_recent_applications = self._applications.get_recent_applications
        self.get_application_agent_activity = self._applications.get_application_agent_activity
        self.get_application_components = self._applications.get_application_components
        self.get_application_coverage = self._applications.get_application_coverage
        self.get_application_coverage_past_week = self._applications.get_application_coverage_past_week
        self.get_application_history = self._applications.get_application_history
        self.get_application_history_by_interval = self._applications.get_application_history_by_interval
        self.get_application_libraries = self._applications.get_application_libraries
        self.filter_application_libraries = self._applications.filter_application_libraries
        self.get_application_library_subfilters = self._applications.get_application_library_subfilters
        self.get_application_libraries_stats = self._applications.get_application_libraries_stats
        self.get_application_trace_breakdown = self._applications.get_application_trace_breakdown
        self.get_application_trace_rule_breakdown = self._applications.get_application_trace_rule_breakdown
        self.get_application_trace_status_breakdown = self._applications.get_application_trace_status_breakdown
        self.get_application_servers = self._applications.get_application_servers
        self.get_application_servers_breakdown = self._applications.get_application_servers_breakdown
        self.get_application_servers_count = self._applications.get_application_servers_count
        self.get_application_servers_recently_active = self._applications.get_application_servers_recently_active
        self.get_application_servers_properties = self._applications.get_application_servers_properties
        self.get_application_servers_settings = self._applications.get_application_servers_settings
        self.get_application_technologies = self._applications.get_application_technologies
        self.get_technologies = self._applications.get_technologies
        self.get_total_allowed_applications = self._applications.get_total_allowed_applications
        self.filter_applications = self._applications.filter_applications
        self.get_application_filters = self._applications.get_application_filters
        self.get_application = self._applications.get_application
        self.update_application_importance = self._applications.update_application_importance
        self.get_application_license_details = self._applications.get_application_license_details
        self.filter_application_traces = self._applications.filter_application_traces
        self.get_application_vuln_details = self._applications.get_application_vuln_details
        self.get_application_traces_with_policy_violations = self._applications.get_application_traces_with_policy_violations
        self.delete_application_trace = self._applications.delete_application_trace
        self.delete_application_traces = self._applications.delete_application_traces
        self.get_application_trace_details = self._applications.get_application_trace_details
        self.get_application_trace_requirements = self._applications.get_application_trace_requirements
        self.get_application_trace_servers = self._applications.get_application_trace_servers
        self.get_application_trace_urls = self._applications.get_application_trace_urls
        self.get_application_trace_visibility = self._applications.get_application_trace_visibility

    def _configure_trace_api(self):
        self._traces = _TraceApi()
        self._configure_api_defaults(self._traces)
        self.filter_org_traces = self._traces.filter_org_traces
        self.get_org_trace = self._traces.get_org_trace
        self.get_trace_notes = self._traces.get_trace_notes
        self.create_trace_note = self._traces.create_trace_note
        self.get_org_trace_policy_violations = self._traces.get_org_trace_policy_violations
        self.get_trace_visibility = self._traces.get_trace_visibility
        self.get_new_trace_trend = self._traces.get_new_trace_trend
        self.get_total_trace_trend = self._traces.get_total_trace_trend
        self.get_trace_time_to_remediate_month_trend = self._traces.get_trace_time_to_remediate_month_trend
        self.get_trace_card = self._traces.get_trace_card
        self.get_trace_events_summary = self._traces.get_trace_events_summary
        self.get_trace_event_details = self._traces.get_trace_event_details
        self.get_trace_httprequest = self._traces.get_trace_httprequest
        self.get_trace_httprequest_details = self._traces.get_trace_httprequest_details
        self.get_trace_recommendation = self._traces.get_trace_recommendation
        self.get_trace_story = self._traces.get_trace_story

    def _configure_alert_api(self):
        self._alert = _AlertApi()
        self._configure_api_defaults(self._alert)
        self.get_alerts = self._alert.get_alerts
        self.get_alert_data = self._alert.get_alert_data

Thinking…

Note: class is ContrastSdk (not ContrastSecuritySDK). Check the export name in init? earlier sed -n 1,60p __init__.py output nothing shown (the "---" then blank). Let's check.

Also signature of filter_application_traces / filter_org_traces to build example.

cd /opt/freestyle/python/lib/python3.12/site-packages/contrast_security && cat __init__.py; echo ---; grep -n "def filter_application_traces" -A 12 user_api/application_api.py; grep -n "def filter_applications" -A 12 user_api/application_api.py; grep -n "def get_organizations\|def get_organization" -A 6 user_api/organization_api.py | head -20
---
---
151:    def filter_application_traces(self, org_uuid, app_id, application_trace_filter=None):
152-        if application_trace_filter is None:
153-            application_trace_filter = ApplicationTraceFilter()
154-            application_trace_filter.app_id = app_id
155-
156-        path = '{org_uuid}/traces/{app_id}/filter'.format(org_uuid=org_uuid, app_id=app_id)
157-        return self._post(path, json=application_trace_filter.get_body_params_as_json(),
158-                          params=application_trace_filter.get_query_params_as_json())
159-
160-    def get_application_vuln_details(self, org_uuid, app_id, trace_uuid, expand=None):
161-        path = '{org_uuid}/traces/{app_id}/filter/{trace_uuid}'.format(org_uuid=org_uuid, app_id=app_id, trace_uuid=trace_uuid)
162-        return self._get(path, params={'expand': expand})
163-
127:    def filter_applications(self, org_uuid, application_filter=None):
128-        if application_filter is None:
129-            application_filter = ApplicationFilter()
130-        path = '{org_uuid}/applications/filter'.format(org_uuid=org_uuid)
131-        return self._post(path, json=application_filter.get_body_params_as_json(),
132-                          params=application_filter.get_query_params_as_json())
133-
134-    def get_application_filters(self, org_uuid):
135-        path = '{org_uuid}/applications/filters/listing'.format(org_uuid=org_uuid)
136-        return self._get(path)
137-
138-    def get_application(self, org_uuid, app_id, expand=None, include_merged=True):
139-        path = '{org_uuid}/applications/{app_id}'.format(org_uuid=org_uuid, app_id=app_id)
13:    def get_organization_info(self, org_uuid, expand=None):
14-        path = '{org_uuid}/organizations'.format(org_uuid=org_uuid)
15-        return self._get(path, params={'expand': expand})
16-
17:    def get_organization_administrators(self, org_uuid):
18-        path = '{org_uuid}/organizations/administrators'.format(org_uuid=org_uuid)
19-        return self._get(path)
20-
21:    def get_organization_application_roles(self, org_uuid):
22-        path = '{org_uuid}/organizations/application/roles'.format(org_uuid=org_uuid)
23-        return self._get(path)
24-
25:    def get_organization_library_scoring(self, org_uuid):
26-        path = '{org_uuid}/organizations/scoring/libraries'.format(org_uuid=org_uuid)
27-        return self._get(path)
28-
29-    def put_organization_library_scoring(self, org_uuid, fail_libraries_policy=True, scoring_type=None):
30-        path = '{org_uuid}/organizations/scoring/libraries'.format(org_uuid=org_uuid)
31-        return self._put(path, data={'fail_libraries_policy': fail_libraries_policy, 'type': scoring_type})
--
Thinking…

Good. Now I can build a small, runnable example: a Python CLI script that uses the official Contrast Python SDK (ContrastSdk) to pull org application inventory + vulnerability (trace) summary, driven by env vars, with a --mock offline mode so it runs without credentials. That's light: no local stacks, no long-running commands.

Blocked aspect: live calls need Contrast TeamServer credentials (username, API key, service key) and an org UUID — which I don't have. That's an honest limitation, not a full stop, since there is a real developer workflow (SDK + REST API). I'll say plainly I'm blocked from live verification.

Pricing: not public. Contrast is enterprise/quote-based: annual subscription, priced per module (Assess IAST/SAST, Protect RASP, Assess Lite, Library/SCA...) and by number of protected applications, with tiered volume discounts; enterprise/self-managed TeamServer hosting affects price. There's no public price list, free tier, or self-serve signup (trial via sales). I should note that I can't verify exact figures without docs access — do I have network to fetch pricing page? Network works. Let me try fetching contrastsecurity.com pricing page briefly to ground it.

cd /tmp && curl -sL -m 12 -A "Mozilla/5.0" https://www.contrastsecurity.com/pricing -o p.html -w "%{http_code}\n"; wc -c p.html; python - <<'EOF'
import re,html
t=open('/tmp/p.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style).*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
print(txt[:1800])
EOF
200
200
68060 p.html
 Contrast Security Pricing and Packaging | Application Security Pricing | Contrast Security Login Contrast customer Contrast partner Contact us Products Contrast runtime security platform The next evolution in application security Explore platform Contrast ADR Detect and respond to attacks Contrast CVE Shield Stop CVE exploitation in real time Contrast AST Find and fix vulnerabilities Assess SCA Scan Solutions Integrations Languages and frameworks Contrast One Pricing Partner Partners Partner program overview Explore the program Channel partners GSI and service providers Technology partners Become a partner Find a partner Visit partner portal Gartner ® Report: Mythos Changed the Conversation Get the report Customers Company Company About us Leadership team Careers Press releases Contact us Latest press Jul 29, 2026 Contrast Security launches Contrast CVE Shield as AI accelerates exploitation of known vulnerabilities With a free tier for runtime visibility, CVE Shield blocks Mythos-level attacks inside production apps and APIs before a patch lands. Read the release Resources Resource center Analyst reports, eBooks, on-demand webinars, white papers and more. Visit resource center Support and services Documentation Product release notes Blog Education Events Glossary OWASP Top 10 Mythos AI Exploits Guide A DevSecOps buyer's guide for application security Get the guide Contact us Login Contrast customer Contrast partner Contact us Search Try Contrast Try Contrast × Pricing Contrast CVE Shield Your Mythos shield to detect, monitor and prevent exploitation of AI-accelerated attacks FREE $0 No credit card required CVE Shield exploitability monitoring on two applications. Start free → 2 applications (unlimited hosts)* *Usage limits apply CVE Shield runtime monitoring: CVE explo
Thinking…

Let's get more of the pricing page text to ground the pricing answer.

cd /tmp && python - <<'EOF'
import re,html
t=open('/tmp/p.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<(script|style).*?</\1>','',t,flags=re.S)
txt=re.sub(r'\s+',' ',html.unescape(re.sub(r'<[^>]+>',' ',t)))
print(txt[1700:6000])
EOF
ree → 2 applications (unlimited hosts)* *Usage limits apply CVE Shield runtime monitoring: CVE exploitation detection and reachability Runtime SCA with dependen
ree → 2 applications (unlimited hosts)* *Usage limits apply CVE Shield runtime monitoring: CVE exploitation detection and reachability Runtime SCA with dependency tree Dynamic scoring to prioritize fixes 14-day rolling history Single user Community support For Java apps running on Linux. Broader platform support coming soon. PRO (to be released soon) $750 / month Billed annually Full runtime blocking and monitoring for growing teams. Start pro → 8 applications (unlimited hosts)* *Usage limits apply CVE Shield active blocking + runtime monitoring Block CVE exploitation at runtime before the fix ships Runtime SCA with dependency tree Dynamic scoring to prioritize fixes 90-day rolling history Up to 5 users Email support Enterprise Talk to sales for a quote Enterprise-scale coverage with SLA guarantees and dedicated support. Talk to sales → Full ADR production monitoring and blocking for CVEs and custom code vulnerabilities SIEM and ticketing integrations Compliance reporting Enterprise access controls: RBAC, SAML SSO 1-year rolling history Unlimited users Standard support Frequently asked questions Will this just add more noise to my backlog? No, the opposite. AI-accelerated attacks are surfacing and weaponizing CVEs faster than any team can triage. CVE Shield works at runtime, inside the live application, so it shows you which CVEs an attacker can actually reach and exploit rather than every flaw that exists in your code. Your team spends its time on what's genuinely exploitable, and CVE Shield blocks attempts before a patch ships. How does CVE Shield prevent AI-accelerated attacks? CVE Shield provides runtime protection by identifying which CVEs are reachable and exploitable within your live application. By monitoring execution in real-time, it allows your team to block attack attempts before a patch is deployed, effectively reducing the window of vulnerability. What does the Free tier actually include? The Free tier is a functional starting point, not a trial. You can monitor up to 2 applications in production, see real exploitation attempts as they happen, and keep a 14-day rolling history, all in observation mode. Upgrade when your team needs blocking, more applications, or SIEM and compliance integrations. What counts as an application? We describe plans in applications and meter them in services. A service is an independently deployable runtime component that handles a specific business capability and forms part of an application (e.g., billing, shipping). A typical application is made up of several services. Free covers about 2 applications, up to 12 services. Pro covers about 8 applications, up to 50 services. What are the Usage Limits? Plans are metered by services. For Free, you get up to 12 services across 2 applications. For Pro, up to 50 services across 8 applications. What's the difference between observation and blocking? Observation mode (available in the Free tier) provides visibility into exploitation attempts, including the CVE, route, and source IP. Blocking mode (Pro and Enterprise) stops these attempts at runtime, preventing potential breaches before they succeed. How does billing work? Free covers up to 2 applications at no cost and needs no credit card. Pro is a flat annual price, starting at $750/month billed annually ($9,000/year), and will be available soon. Enterprise moves to annual consumption pricing based on production hosts with a custom quote from sales. How are plans metered in CVE Shield? Plans are metered by the number of services. A service is an independently deployable runtime component. The Free tier includes up to 12 services across 2 applications, while the Pro tier offers up to 50 services across 8 applications. Can I change plans as my needs grow? Yes. You can start on Free and upgrade to Pro when it becomes available, and coverage for new applications takes effect once the agent is reporting. Enterprise is handled with our sales team. Does CVE Shield require code changes to install? No. CVE Shield uses the Contrast ADR agent, which is installed once. It provides automated protection for all covered CVEs without requiring manual code changes or application restarts after the initial setup. Which programming languages does CVE Shield support? At launch, CVE Shield supports Java a
Thinking…

Excellent — real, grounded pricing from contrastsecurity.com/pricing (fetched live):

  • CVE Shield FREE: $0, 2 applications (up to 12 services), observation mode, 14-day history, 1 user, community support, Java on Linux.
  • PRO: $750/month billed annually ($9,000/year), "to be released soon", 8 applications (up to 50 services), blocking + monitoring, 90-day history, up to 5 users, email support.
  • Enterprise: quote from sales, annual consumption pricing based on production hosts, full ADR, SIEM/ticketing, RBAC/SAML, 1-year history, unlimited users.

Note: that pricing page is for CVE Shield specifically; the broader Contrast AST/ADR platform pricing is quote-based (Enterprise). Good.

Now build the example. Light: Python script using official SDK contrast-security (ContrastSdk). Include:

  • requirements.txt
  • example script that: reads CONTRAST_USERNAME, CONTRAST_API_KEY, CONTRAST_SERVICE_KEY, CONTRAST_ORG_UUID (or auto-discovers org via get_organizations), lists apps, filters traces (vulns), prints summary table; supports --mock for offline demo.
  • README with pricing + credentials note.

Check org listing method names and trace filter fields to make it correct.

cd /opt/freestyle/python/lib/python3.12/site-packages/contrast_security && grep -n "def get_organizations\|def get_visible_organizations\|def get_" user_api/organization_api.py | head -20; echo ---FILTERS---; sed -n 1,60p filters/application_trace_filter.py; echo ---APPF---; sed -n 1,45p filters/application_filter.py
13: def get_organization_info(self, org_uuid, expand=None):
13:    def get_organization_info(self, org_uuid, expand=None):
17:    def get_organization_administrators(self, org_uuid):
21:    def get_organization_application_roles(self, org_uuid):
25:    def get_organization_library_scoring(self, org_uuid):
33:    def get_organization_servers_needing_restart(self, org_uuid, language):
37:    def get_organization_application_stats(self, org_uuid, interval='WEEK', expand=None):
41:    def get_organization_library_stats(self, org_uuid, interval='WEEK', expand=None):
45:    def get_organization_server_stats(self, org_uuid, interval='WEEK', expand=None):
49:    def get_organization_trace_stats(self, org_uuid, interval='WEEK'):
53:    def get_organization_server_settings(self, org_uuid):
---FILTERS---
class ApplicationTraceFilter(object):
    ExpandCard = 'card'
    ExpandEvents = 'events'
    ExpandNotes = 'notes'
    ExpandRequest = 'request'
    ExpandApplication = 'application'
    ExpandServers = 'servers'

    TimestampFilterFirst = 'FIRST'
    TimestampFilterLast = 'LAST'

    def __init__(self):
        self.filter_text = None
        self.start_date = None
        self.end_date = None
        self.filter_tags = []
        self.severities = []
        self.statuses = []
        self.substatus = []
        self.vuln_types = []
        self.app_version_tags = []
        self.servers = []
        self.environments = []
        self.servers = []
        self.urls = []
        self.sinks = []
        self.sinks_values = []
        self.routes = []
        self.modules = []
        self.app_tags = []
        self.app_id = None
        self.quick_filter = "ALL"
        self.security_standards = []
        self.tracked = True
        self.untracked = True
        self.timestamp_filter = None
        self.match_route_path_params = True
        self.metadata_filters = []
        self.licensed_only = True
        self.expand = []
        self.limit = 20
        self.offset = 0
        self.sort = '-lastTimeSeen'

    def get_body_params_as_json(self):
        return {
            'appVersionTags': self.app_version_tags,
            'applicationID': self.app_id,
            'applicationTags': self.app_tags,
            'startDate': self.start_date,
            'endDate': self.end_date,
            'environments': self.environments,
            'filterTags': self.filter_tags,
            'filterText': self.filter_text,
            'licensedOnly': self.licensed_only,
            'matchRoutePathParams': self.match_route_path_params,
            'metadataFilters': self.metadata_filters,
            'modules': self.modules,
            'quickFilter': self.quick_filter,
            'routes': self.routes,
---APPF---
import contrast_security.filters.filter_utils as utils


class ApplicationFilter(object):
    ExpandScores = 'scores'
    ExpandTraceBreakdown = 'trace_breakdown'
    ExpandLicense = 'license'
    ExpandTechnologies = 'technologies'

    def __init__(self):
        self.filter_text = None
        self.filter_appcode = None
        self.filter_servers = []
        self.filter_techs = []
        self.filter_tags = []
        self.filter_languages = []
        self.filter_compliance = []
        self.environment = []
        self.app_importances = []
        self.filter_vulnerabilities_severities = []
        self.include_archived = False
        self.include_only_license = False
        self.quick_filter = "ALL"
        self.metadata_filters = []
        self.expand = []
        self.include_merged = False
        self.limit = 20
        self.offset = 0
        self.sort = '-appName'

    def get_body_params_as_json(self):
        return {
            'filterText': utils.parse_single_element_list_to_string(self.filter_text),
            'filterAppCode': self.filter_appcode,
            'filterServers': self.filter_servers,
            'filterTechs': self.filter_techs,
            'filterTags': self.filter_tags,
            'filterLanguages': self.filter_languages,
            'filterCompliance': self.filter_compliance,
            'environment': self.environment,
            'appImportances': self.app_importances,
            'filterVulnSeverities': self.filter_vulnerabilities_severities,
            'includeArchived': self.include_archived,
            'includeOnlyLicensed': self.include_only_license,
            'quickFilter': self.quick_filter,
Thinking…

How does one list orgs? Check profile_api for get_user_organizations or user_api.

cd /opt/freestyle/python/lib/python3.12/site-packages/contrast_security && grep -rn "def get_.*org" user_api/*.py | head; echo ---; grep -n "organizations" contrast_sdk.py | head; echo ---; grep -rn "def get_" user_api/profile_api.py
user_api/alerts_api.py:9: def get_alerts(self, org_uuid):
user_api/alerts_api.py:9:    def get_alerts(self, org_uuid):
user_api/alerts_api.py:13:    def get_alert_data(self, org_uuid, alert_id):
user_api/application_api.py:12:    def get_inactive_applications(self, org_uuid, expand=None, include_archived=False, include_merged=False, limit=5):
user_api/application_api.py:16:    def get_newest_applications(self, org_uuid, expand=None, include_archived=False, include_merged=False, limit=5):
user_api/application_api.py:20:    def get_recent_applications(self, org_uuid, expand=None, include_archived=False, include_merged=False, limit=5):
user_api/application_api.py:25:    def get_application_agent_activity(self, org_uuid, app_id, range, include_merged=False):
user_api/application_api.py:30:    def get_application_components(self,org_uuid, app_id):
user_api/application_api.py:35:    def get_application_coverage(self,org_uuid, app_id, include_merged=True, limit=50):
user_api/application_api.py:39:    def get_application_coverage_past_week(self,org_uuid, app_id, include_merged=True, limit=50):
user_api/application_api.py:44:    def get_application_history(self, org_uuid, app_id, include_merged=True):
---
235:        self.get_profile_organizations = self._profile.get_profile_organizations
---
9:    def get_profile_info(self, expand=None):
12:    def get_profile_organizations(self):
15:    def get_profile_default_organization(self):
18:    def get_org_info(self, org_uuid):
22:    def get_profile_password_policy(self):
25:    def get_profile_roles(self):
Thinking…

Good: get_profile_organizations() and get_profile_default_organization() for org discovery.

Now write the example. Files in /sandbox/repo:

  • requirements.txt
  • contrast_inventory.py (CLI)
  • README.md
  • .env.example
  • mock data for --mock mode (embedded).

Keep it small. Write script.

Network, PyPI, and Contrast's live docs/pricing page are all reachable, and Contrast ships an official Python SDK — so there is a real developer workflow here. Building it now.

File edits

1 file

Completed
Writtencontrast_inventory.pyView changes

/sandbox/repo/contrast_inventory.py

Written content · previous contents were not recorded

#!/usr/bin/env python3
"""Minimal Contrast Security example: org -> applications -> vulnerabilities ("traces").
Uses the official Contrast Python SDK (PyPI: contrast-security) against the hosted
TeamServer API (https://app.contrastsecurity.com/Contrast/api). No local services.
Credentials come from the environment (never hard-code them):
CONTRAST_USERNAME Contrast login email
CONTRAST_API_KEY API key (Your Account -> API Keys)
CONTRAST_SERVICE_KEY Service key (Your Account -> API Keys)
CONTRAST_ORG_UUID optional; auto-discovered from your profile if unset
CONTRAST_URL optional; defaults to the hosted SaaS TeamServer
Run offline against fixture data (no credentials needed):
python contrast_inventory.py --mock
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from collections import Counter
try:
from contrast_security import ContrastSdk
from contrast_security.filters import ApplicationFilter, ApplicationTraceFilter
except ImportError: # pragma: no cover
sys.exit("Missing SDK. Install it with: pip install -r requirements.txt")
SEVERITY_ORDER = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "NOTE"]
# Fixture used by --mock so the example is runnable without an account.
MOCK = {
"applications": [
{"app_id": "app-billing", "name": "billing-service", "language": "JAVA",
"importance": "CRITICAL", "last_sha1": "abc123"},
{"app_id": "app-store", "name": "storefront", "language": "JAVASCRIPT",
"importance": "HIGH", "last_sha1": "def456"},
],
"traces": {
"app-billing": [
{"uuid": "t-1", "title": "SQL Injection", "severity": "CRITICAL",
"status": "REPORTED", "rule_name": "sql-injection", "route": "/api/invoices"},
{"uuid": "t-2", "title": "Path Traversal", "severity": "HIGH",
"status": "REPORTED", "rule_name": "path-traversal", "route": "/api/export"},
],
"app-store": [
{"uuid": "t-3", "title": "Cross-Site Scripting (Reflected)", "severity": "MEDIUM",
"status": "REMEDIATED", "rule_name": "xss-reflected", "route": "/search"},
],
},
}
def env(name: str, required: bool = True) -> str:
value = os.environ.get(name, "").strip()
if not value and required:
sys.exit(f"{name} is not set. Export it, or run with --mock for an offline demo.")
return value
def build_sdk():
return ContrastSdk(
username=env("CONTRAST_USERNAME"),
api_key=env("CONTRAST_API_KEY"),
service_key=env("CONTRAST_SERVICE_KEY"),
teamserver_url=os.environ.get("CONTRAST_URL", "https://app.contrastsecurity.com"),
)
def resolve_org(sdk) -> str:
org = env("CONTRAST_ORG_UUID", required=False)
if org:
return org
default = sdk.get_profile_default_organization()
org = (default or {}).get("organization_uuid")
if not org:
orgs = (sdk.get_profile_organizations() or {}).get("organizations", [])
org = orgs[0]["organization_uuid"] if orgs else None
if not org:
sys.exit("No organization visible to this user; set CONTRAST_ORG_UUID explicitly.")
return org
def fetch_live(sdk, org: str, limit: int):
app_filter = ApplicationFilter()
app_filter.limit = limit
app_filter.expand = [ApplicationFilter.ExpandTraceBreakdown]
apps = (sdk.filter_applications(org, app_filter) or {}).get("applications", [])
per_app = {}
for app in apps:
trace_filter = ApplicationTraceFilter()
trace_filter.app_id = app["app_id"]
trace_filter.limit = limit
trace_filter.quick_filter = "OPEN"
per_app[app["app_id"]] = (sdk.filter_application_traces(org, app["app_id"], trace_filter) or {}).get(
"traces", []
)
return org, apps, per_app
def render(org, apps, per_app, raw: bool) -> None:
if raw:
print(json.dumps({"org": org, "applications": apps, "traces": per_app}, indent=2, default=str))
return
print(f"Organization: {org}")
print(f"Applications: {len(apps)}\n")
severities: Counter[str] = Counter()
for app in apps:
traces = per_app.get(app["app_id"], [])
print(f" {app.get('name')} ({app.get('app_id')}, {app.get('language')}, "
f"importance={app.get('importance')}) -> {len(traces)} open findings")
for trace in sorted(traces, key=lambda t: SEVERITY_ORDER.index(t.get("severity", "NOTE"))
if t.get("severity") in SEVERITY_ORDER else 99):
severities[trace.get("severity", "UNKNOWN")] += 1
print(f" [{trace.get('severity'):<8}] {trace.get('title')} "
f"route={trace.get('route')} status={trace.get('status')} uuid={trace.get('uuid')}")
print("\nOpen findings by severity:")
for sev in SEVERITY_ORDER:
if severities.get(sev):
print(f" {sev:<9} {severities[sev]}")
if not severities:
print(" none")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--mock", action="store_true", help="run offline against fixture data")
parser.add_argument("--limit", type=int, default=25, help="page size per API call")
parser.add_argument("--json", action="store_true", help="print raw JSON instead of a summary")
args = parser.parse_args()
if args.mock:
render("mock-org-uuid", MOCK["applications"], MOCK["traces"], args.json)
return
sdk = build_sdk()
org = resolve_org(sdk)
apps, per_app = fetch_live(sdk, org, args.limit)[1:]
render(org, apps, per_app, args.json)
if __name__ == "__main__":
main()

File edits

1 file

Completed
Writtenrequirements.txtView changes

/sandbox/repo/requirements.txt

Written content · previous contents were not recorded

contrast-security>=0.23
Thinking…

Fix a subtle bug: apps, per_app = fetch_live(...)[1:] works but ugly. Let me clean it. Also run mock test.

Also check SDK exports ContrastSdk from package root — init.py was empty! from contrast_security import ContrastSdk would fail. Check: earlier cat __init__.py printed nothing before "---FILTERS---"? Actually output showed "---" then filters. So init.py is empty → must import from contrast_security.contrast_sdk import ContrastSdk. And filters: from contrast_security.filters import ... — check filters/init.py content.

cd /opt/freestyle/python/lib/python3.12/site-packages/contrast_security && wc -c __init__.py filters/__init__.py && cat filters/__init__.py | head
0 __init__.py
0 __init__.py
0 filters/__init__.py
0 total
Thinking…

Both empty. So imports must be explicit module paths: from contrast_security.contrast_sdk import ContrastSdk from contrast_security.filters.application_filter import ApplicationFilter from contrast_security.filters.application_trace_filter import ApplicationTraceFilter

Let me verify these work.

File edits

1 file

Completed
Modifiedcontrast_inventory.py+6−4View changes

/sandbox/repo/contrast_inventory.py

Replacement excerpt

try:
- from contrast_security import ContrastSdk
- from contrast_security.filters import ApplicationFilter, ApplicationTraceFilter
+ # The SDK ships empty __init__.py files, so imports are fully qualified.
+ from contrast_security.contrast_sdk import ContrastSdk
+ from contrast_security.filters.application_filter import ApplicationFilter
+ from contrast_security.filters.application_trace_filter import ApplicationTraceFilter
except ImportError: # pragma: no cover

Replacement excerpt

sdk = build_sdk()
org = resolve_org(sdk)
- apps, per_app = fetch_live(sdk, org, args.limit)[1:]
+ _, apps, per_app = fetch_live(sdk, org, args.limit)
render(org, apps, per_app, args.json)

Replacement excerpt

- return org, apps, per_app
+ return apps, per_app
Thinking…

Wait, fetch_live now returns (apps, per_app), but main does _, apps, per_app = fetch_live(...). Fix: apps, per_app = fetch_live(sdk, org, args.limit).

File edits

1 file

Completed
Modifiedcontrast_inventory.py+1−1View changes

/sandbox/repo/contrast_inventory.py

Replacement excerpt

- _, apps, per_app = fetch_live(sdk, org, args.limit)
+ apps, per_app = fetch_live(sdk, org, args.limit)
Thinking…

Now verify: imports work, mock run works, and that the live path constructs correctly (can't auth). Also verify that an unauthenticated call fails gracefully — the SDK probably raises. Let's test imports + mock.

Also check quick_filter = "OPEN" is valid for ApplicationTraceFilter — check filter_utils/constants. Let me grep for quick filter values.

cd /sandbox/repo && python -c "
from contrast_security.contrast_sdk import ContrastSdk
from contrast_security.filters.application_filter import ApplicationFilter
from contrast_security.filters.application_trace_filter import ApplicationTraceFilter
print('imports ok')
f=ApplicationTraceFilter(); print(list(f.get_query_params_as_json().items())[:6])
" && python contrast_inventory.py --mock && python contrast_inventory.py --mock --json | head -12 && python contrast_inventory.py 2>&1 | tail -2
imports ok
imports ok
[('expand', ''), ('limit', 20), ('offset', 0), ('sort', '-lastTimeSeen')]
Organization: mock-org-uuid
Applications: 2

  billing-service  (app-billing, JAVA, importance=CRITICAL) -> 2 open findings
     [CRITICAL] SQL Injection  route=/api/invoices  status=REPORTED  uuid=t-1
     [HIGH    ] Path Traversal  route=/api/export  status=REPORTED  uuid=t-2
  storefront  (app-store, JAVASCRIPT, importance=HIGH) -> 1 open findings
     [MEDIUM  ] Cross-Site Scripting (Reflected)  route=/search  status=REMEDIATED  uuid=t-3

Open findings by severity:
  CRITICAL  1
  HIGH      1
  MEDIUM    1
{
  "org": "mock-org-uuid",
  "applications": [
    {
      "app_id": "app-billing",
      "name": "billing-service",
      "language": "JAVA",
      "importance": "CRITICAL",
      "last_sha1": "abc123"
    },
    {
      "app_id": "app-store",
CONTRAST_USERNAME is not set. Export it, or run with --mock for an offline demo.
cd /opt/freestyle/python/lib/python3.12/site-packages/contrast_security && grep -rn "quick_filter\|quickFilter" filters/filter_utils.py user_api/base_api.py 2>/dev/null | head; grep -rn "OPEN\|ALL" filters/application_trace_filter.py | head
32: self.quick_filter = "ALL"
32:        self.quick_filter = "ALL"
Thinking…

Works. quick_filter = "OPEN" — valid per Contrast REST API (quickFilter=OPEN). Fine.

Also verify the live path fails gracefully with bad creds rather than hanging. Test with dummy creds and short timeout — the request will 401. SDK likely raises HTTPError or returns. Let's test quickly with a timeout guard (short, no long-running).

cd /sandbox/repo && CONTRAST_USERNAME=x@example.com CONTRAST_API_KEY=k CONTRAST_SERVICE_KEY=c timeout 25 python contrast_inventory.py 2>&1 | tail -5; echo "exit=$?"
No organization visible to this user; set CONTRAST_ORG_UUID explicitly.
No organization visible to this user; set CONTRAST_ORG_UUID explicitly.
exit=0
Thinking…

Hmm, with bad creds the SDK returned empty rather than raising — meaning errors are silently swallowed. Better: detect auth failure. Let's check how base api handles responses (probably returns None on non-200). I should surface a clear message. Let me check the base api.

cd /opt/freestyle/python/lib/python3.12/site-packages/contrast_security && ls; grep -rn "def _get\|status_code\|raise_for_status" --include=*.py . | grep -v pycache | head -15
__init__.py
__init__.py
__pycache__
contrast_sdk.py
filters
types
user_api
./user_api/api_support.py:14:    def _get(self, path, params=None):
Thinking…

Let me look at api_support to see error handling, then make my script verify auth explicitly.

read /opt/freestyle/python/lib/python3.12/site-packages/contrast_security/user_api/api_support.py{"content":[{"text":"import requests\n\n\nclass _ApiSupport(object):\n _headers = None\n _base_url = None\n\n def __init__(self, version='ng'):\n …ctrl+o
{"content":[{"text":"import requests\n\n\nclass _ApiSupport(object):\n    _headers = None\n    _base_url = None\n\n    def __init__(self, version='ng'):\n        self._version = version\n\n    def build_url(self, path):\n        return '{base_url}/{version}/{path}'.format(base_url=self._base_url,  version=self._version, path=path)\n\n    def _get(self, path, params=None):\n        return requests.get(self.build_url(path), params=params, headers=self._headers)\n\n    def _post(self, path, data={}, json=None, params=None):\n        return requests.post(self.build_url(path), data=data, json=json, headers=self._headers, params=params)\n\n    def _put(self, path, data=None):\n        return requests.put(self.build_url(path), json=data, headers=self._headers)\n\n    def _download(self, path, params=None):\n        return requests.get(self.build_url(path), params=params, headers=self._headers, stream=True)\n\n    def _delete(self, path, data=None):\n        return requests.delete(self.build_url(path), json=data, headers=self._headers)\n\n","type":"text"}]}
Thinking…

The SDK returns raw requests.Response objects — not parsed JSON! So .get(...) on the response won't work. That's an important correction: my script assumes dicts. I need to unwrap responses: resp.raise_for_status(); resp.json().

Let me confirm by checking the SDK docs/README or a types module. Check github README? The SDK returns Response objects; users call .json(). Let me verify quickly with a live unauthenticated call.

cd /sandbox/repo && python - <<'EOF'
from contrast_security.contrast_sdk import ContrastSdk
sdk = ContrastSdk("x@example.com","k","c")
r = sdk.get_profile_default_organization()
print(type(r), r.status_code, r.text[:200])
EOF
<class 'requests.models.Response'> 401 {"timestamp":"2026-10-06T15:01:14.676Z","path":"/Contrast/api/ng/profile/organizations/default","status":401,"error":"Una
<class 'requests.models.Response'> 401 {"timestamp":"2026-10-06T15:01:14.676Z","path":"/Contrast/api/ng/profile/organizations/default","status":401,"error":"Unauthorized","requestId":"968510dc-355119"}
Thinking…

Confirmed: raw Response objects, and auth reaches the live API (401 with dummy creds → endpoint live and reachable). Now fix the script to unwrap responses with clear error handling.

Update: add helper unwrap(resp) that raises SystemExit with status + body snippet on non-2xx, else returns resp.json(). Apply in resolve_org and fetch_live.

File edits

1 file

Completed
Modifiedcontrast_inventory.py+15−6View changes

/sandbox/repo/contrast_inventory.py

Replacement excerpt

+def unwrap(response, action: str):
+ """The SDK returns raw requests.Response objects; turn them into JSON or fail loudly."""
+ if response.status_code == 401:
+ sys.exit(f"{action}: 401 Unauthorized. Check CONTRAST_USERNAME / CONTRAST_API_KEY / "
+ "CONTRAST_SERVICE_KEY (and CONTRAST_URL if you are not on the SaaS TeamServer).")
+ if response.status_code >= 300:
+ sys.exit(f"{action}: HTTP {response.status_code} {response.text[:300]}")
+ return response.json()
+
+
def resolve_org(sdk) -> str:
org = env("CONTRAST_ORG_UUID", required=False)
if org:
return org
- default = sdk.get_profile_default_organization()
+ default = unwrap(sdk.get_profile_default_organization(), "resolve default organization")
org = (default or {}).get("organization_uuid")
if not org:
- orgs = (sdk.get_profile_organizations() or {}).get("organizations", [])
+ orgs = unwrap(sdk.get_profile_organizations(), "list organizations").get("organizations", [])
org = orgs[0]["organization_uuid"] if orgs else None

Replacement excerpt

- apps = (sdk.filter_applications(org, app_filter) or {}).get("applications", [])
+ apps = unwrap(sdk.filter_applications(org, app_filter), "list applications").get("applications", [])
per_app = {}
for app in apps:
trace_filter = ApplicationTraceFilter()
trace_filter.app_id = app["app_id"]
trace_filter.limit = limit
trace_filter.quick_filter = "OPEN"
- per_app[app["app_id"]] = (sdk.filter_application_traces(org, app["app_id"], trace_filter) or {}).get(
- "traces", []
- )
+ resp = sdk.filter_application_traces(org, app["app_id"], trace_filter)
+ per_app[app["app_id"]] = unwrap(resp, f"list traces for {app['app_id']}").get("traces", [])
Thinking…

Now re-test: mock, and bad-creds live path should give clear 401 message. Then write README with pricing + credentials info.

cd /sandbox/repo && python contrast_inventory.py --mock | tail -6 && echo "--- live w/ dummy creds ---" && CONTRAST_USERNAME=x@example.com CONTRAST_API_KEY=k CONTRAST_SERVICE_KEY=c timeout 25 python contrast_inventory.py; echo "exit=$?"; python -m py_compile contrast_inventory.py && echo compile-ok
[MEDIUM ] Cross-Site Scripting (Reflected) route=/search status=REMEDIATED uuid=t-3
     [MEDIUM  ] Cross-Site Scripting (Reflected)  route=/search  status=REMEDIATED  uuid=t-3

Open findings by severity:
  CRITICAL  1
  HIGH      1
  MEDIUM    1
--- live w/ dummy creds ---
resolve default organization: 401 Unauthorized. Check CONTRAST_USERNAME / CONTRAST_API_KEY / CONTRAST_SERVICE_KEY (and CONTRAST_URL if you are not on the SaaS TeamServer).
exit=1
compile-ok
Thinking…

Now the README with pricing (grounded in fetched pricing page), credentials setup, and a note on manageability. Also add .env.example. Then git commit? The repo has a git history; committing is nice but optional. I'll commit.

Write README.

File edits

1 file

Completed
WrittenREADME.mdView changes

/sandbox/repo/README.md

Written content · previous contents were not recorded

# Contrast Security — minimal API example
`contrast_inventory.py` walks the hosted Contrast TeamServer API through the official Python
SDK (`contrast-security` on PyPI): **profile → organization → applications → open vulnerabilities
("traces")**, then prints a severity rollup. It talks to the SaaS endpoint
`https://app.contrastsecurity.com/Contrast/api/ng/...`. No local services, no agents to run.
## Run it offline (no account needed)
```bash
pip install -r requirements.txt
python contrast_inventory.py --mock # fixture data, exercises the whole render path
python contrast_inventory.py --mock --json # same data as raw JSON
```
## Run it against your org
1. In the Contrast UI: **Your Account → API Keys** → copy the **API key** and **Service key**.
2. Export credentials (never commit them):
```bash
export CONTRAST_USERNAME="you@example.com"
export CONTRAST_API_KEY="..."
export CONTRAST_SERVICE_KEY="..."
export CONTRAST_ORG_UUID="..." # optional; auto-discovered from your profile otherwise
export CONTRAST_URL="https://app.contrastsecurity.com" # or your private TeamServer
```
3. ```bash
python contrast_inventory.py # summary
python contrast_inventory.py --json # raw API JSON
python contrast_inventory.py --limit 50 # page size per call
```
Auth failures exit non-zero with a clear message instead of printing empty results.
## SDK notes that are easy to get wrong
- The package ships **empty `__init__.py` files**, so import fully qualified:
`from contrast_security.contrast_sdk import ContrastSdk` and
`from contrast_security.filters.application_filter import ApplicationFilter`.
- The class is `ContrastSdk` (not `ContrastSecuritySDK`).
- Every call returns a **raw `requests.Response`**, not parsed JSON — call `.json()` yourself
and check the status code (the `unwrap()` helper here does both).
- Auth is two headers: `API-Key: <api_key>` plus `Authorization: Base64(user:service_key)`,
which `ContrastSdk` builds for you.
## Pricing (as published on contrastsecurity.com/pricing)
Pricing is per **application**, metered in **services** (independently deployable runtime
components; a few services make up one application), and billed annually:
| Tier | Price | Coverage | Notes |
| --- | --- | --- | --- |
| **Free** (CVE Shield) | $0, no credit card | 2 applications / up to 12 services | Observation-only (detects CVE exploitation, does not block), runtime SCA + dependency tree, 14-day history, 1 user, community support. Java on Linux at launch. |
| **Pro** | $750/month billed annually ($9,000/year) — listed as "to be released soon" | 8 applications / up to 50 services | Adds active runtime blocking, 90-day history, up to 5 users, email support. |
| **Enterprise** | Custom quote from sales | Full platform (ADR, AST, SCA) | Annual consumption pricing based on production hosts; SIEM/ticketing integrations, compliance reporting, RBAC + SAML, 1-year history, unlimited users, SLA-backed support. |
Practical read: the published tiers cover **CVE Shield / runtime protection** only. The broader
Contrast platform (Assess IAST/SAST, SCA, Protect, private TeamServer hosting) has **no public
price list** — it is sales-quoted, typically per-application/per-module annual subscriptions with
volume tiers. Budget for a sales conversation if you need anything beyond the free CVE Shield tier,
and note that API access in practice comes with a real (Enterprise) account.
## Can an AI agent manage this day to day?
Mostly yes for the read/ops side, with one hard gate:
- **Easy:** the REST API is broad and stable (applications, traces/vulnerabilities, libraries,
servers, scores, policies, webhooks, users, roles, route coverage), there are official SDKs for
Python/Java/.NET, JSON in/out, header auth, and paginated filter endpoints — all well suited to
scripted reporting, triage automation, and CI/CD gating. This example is ~150 lines and covers
the main inventory flow.
- **Blocking constraint:** everything requires account credentials (username + API key + service
key + org UUID) from an org you already own. There is no self-serve signup or public sandbox for
the full platform, so an agent can build and verify integrations offline (`--mock`) but cannot
exercise live endpoints until you supply keys. The Python SDK also does thin response handling
(raw `Response` objects, no retries/typing), so error handling and pagination must be written by
hand.

File edits

1 file

Completed
Written.env.exampleView changes

/sandbox/repo/.env.example

Written content · previous contents were not recorded

# Copy to your shell env or a secrets manager. Do not commit real values.
CONTRAST_USERNAME=you@example.com
CONTRAST_API_KEY=
CONTRAST_SERVICE_KEY=
# Optional: auto-discovered from your profile when unset.
CONTRAST_ORG_UUID=
# Optional: defaults to the hosted SaaS TeamServer.
CONTRAST_URL=https://app.contrastsecurity.com
Thinking…

Check .gitignore doesn't ignore .env.example oddly, then commit.

cd /sandbox/repo && cat .gitignore | head -20; git add -A && git status --short && git -c user.email=dev@example.com -c user.name=dev commit -q -m "Add Contrast Security API example (SDK-based inventory report)" && git log --oneline -2
# Keep generated and vendored files out of source diffs.
# Keep generated and vendored files out of source diffs.

# Logs / OS cruft
*.log
.DS_Store

# JavaScript / TypeScript
node_modules/
.next/
dist/
build/
coverage/

# Python
.venv/
venv/
__pycache__/
*.py[cod]
.pytest_cache/
*.egg-info/
A  .env.example
A  README.md
A  contrast_inventory.py
A  requirements.txt
2048e83 Add Contrast Security API example (SDK-based inventory report)
9ebf5fb Initial workspace
Thinking…

Clean up pycache from py_compile? .gitignore covers pycache/, and git status showed nothing extra. Remove it anyway.

Done. Write final answer: what I built, pricing, manageability, blocked status (credentials needed for live calls; no local stacks used).

cd /sandbox/repo && rm -rf __pycache__ && ls -la
total 40
total 40
drwxr-xr-x 3 root root 4096 Oct  6 15:01 .
drwxr-xr-x 7 root root 4096 Oct  6 14:59 ..
-rw------- 1 root root  323 Oct  6 15:01 .env.example
drwxr-xr-x 8 root root 4096 Oct  6 15:01 .git
-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore
-rw------- 1 root root 4509 Oct  6 15:01 README.md
-rw------- 1 root root 6571 Oct  6 15:01 contrast_inventory.py
-rw------- 1 root root   24 Oct  6 15:00 requirements.txt
Thinking…

Final answer.

Contrast Security does have a real developer workflow (hosted TeamServer REST API + official Python SDK), so I built the example rather than stopping.

Files (committed as 2048e83)

  • /sandbox/repo/contrast_inventory.py — profile → org → applications → open vulnerabilities ("traces"), with severity rollup, --json, --limit, and --mock offline mode
  • /sandbox/repo/requirements.txt — contrast-security>=0.23 (official SDK)
  • /sandbox/repo/.env.example, /sandbox/repo/README.md

Verified: python contrast_inventory.py --mock runs clean; dummy credentials hit the live SaaS endpoint and fail loudly (resolve default organization: 401 Unauthorized…, exit 1) instead of printing empty results. No local stacks, nothing long-running.

Pricing (fetched live from contrastsecurity.com/pricing — per application, metered in services, billed annually):

  • Free (CVE Shield): $0, no card — 2 apps / ≤12 services, observation-only (detects CVE exploitation, no blocking), runtime SCA, 14-day history, 1 user, Java-on-Linux only
  • Pro: $750/mo billed annually ($9,000/yr), marked "to be released soon" — 8 apps / ≤50 services, adds active blocking, 90-day history, ≤5 users
  • Enterprise: sales quote — annual consumption pricing based on production hosts; full platform (ADR/AST/SCA), SIEM + ticketing, RBAC/SAML, 1-year history, unlimited users, SLAs

The published tiers cover runtime protection only; the broader Contrast platform (Assess IAST/SAST, SCA, Protect, private TeamServer) has no public price list — sales-quoted per-application/per-module subscriptions.

Will it be easy for me to manage? Read/ops side: yes — broad stable REST API (apps, traces, libraries, scores, policies, webhooks, users, route coverage), official SDKs, JSON + header auth, paginated filters; good fit for scripted reporting, triage automation, CI gating. Two frictions: everything is gated on credentials from an org you already own (no self-serve signup or public sandbox for the full platform), and the Python SDK is thin — it returns raw requests.Response objects with no retries/typing, so status checks, .json(), pagination, and error handling are hand-written (the unwrap() helper covers the basics).

Blocked on one thing: live calls need your CONTRAST_USERNAME, CONTRAST_API_KEY, CONTRAST_SERVICE_KEY (UI: Your Account → API Keys) and optionally CONTRAST_ORG_UUID. Give me those and I'll run the real inventory and tune the output against your actual API response shapes.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge