feat: implement SARIF output and improve redteam test coverage#36
Conversation
Add red team adversarial testing capabilities: - New `quaestor/redteam/` module with DeepTeam adapter - models.py: VulnerabilityType, AttackMethod, AttackResult, RedTeamReport - config.py: YAML-based playbook system (quick, standard, comprehensive, owasp-llm) - adapter.py: DeepTeamAdapter wrapping DeepTeam APIs with mock fallback - runner.py: RedTeamRunner for HTTP/callback agent testing - CLI command `quaestor redteam` with: - Playbook selection (--playbook) - YAML config support (--config) - Mock mode for testing without DeepTeam (--mock) - Console and JSON output formats - Reporting integration: - HTML reports with red team section - SARIF output for CI integration (add_redteam_report) - Verdict conversion for unified reporting - Extended QuaestorInvestigator with probe types and history - 34 new tests for red team module (all passing) Closes #24 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused imports (datetime, UTC) in test_redteam.py - Rename unused loop/function arguments to underscore prefix - Add `from None` / `from e` to re-raised exceptions in cli.py - Remove unused import in _display_redteam_report Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix unused arguments in test_optimizer.py callback functions - Remove unused json import in test_patterns.py (auto-fixed by ruff) - Add noqa comments for intentionally unused API arguments in optimizer.py - Fix unused loop variable in patterns.py These were pre-existing issues in main that blocked the PR CI. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add missing newline at end of probe_types.py - Remove unused AsyncMock import in test_investigator_probing.py - Fix import ordering Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace Optional[X] with X | None - Replace Dict with dict Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Apply consistent formatting across all modified files. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…t coverage - Wire up create_sarif_from_issues() to CLI lint --format sarif - Add 10 new tests for redteam module coverage - Coverage: config.py 96%, runner.py 85%, adapter.py 67% - Overall: 87.76% -> 88.32% Closes #35
There was a problem hiding this comment.
Pull request overview
This pull request implements SARIF output for the lint command and adds comprehensive red team testing capabilities with improved test coverage for the redteam module.
Changes:
- Wires up existing SARIF infrastructure to CLI lint command with
--format sarifoption - Implements complete red team module with DeepTeam integration including models, adapters, configuration, and runner
- Adds 10 new tests for redteam module improving coverage from 77% to 96% for config.py and 75% to 85% for runner.py
Reviewed changes
Copilot reviewed 23 out of 30 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Adds deepteam 1.0.5 package dependency |
| pyproject.toml | Enables redteam optional dependency group |
| quaestor/cli.py | Implements SARIF output in lint command, adds redteam CLI command |
| quaestor/reporting/sarif.py | Adds red team attack result SARIF conversion methods |
| quaestor/reporting/html.py | Adds red team report HTML rendering |
| quaestor/redteam/*.py | New module with models, adapter, config, and runner |
| quaestor/runtime/investigator.py | Adds probe history tracking and session management |
| tests/test_sarif.py | Adds 7 new red team SARIF tests |
| tests/test_redteam.py | New comprehensive test file with 1072 lines |
| tests/test_investigator_probing.py | New probe testing file |
| # Ensure the LM is configured globally for DSPy | ||
| if not dspy.settings.lm: | ||
| dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) | ||
|
|
There was a problem hiding this comment.
The global DSPy LM configuration is initialized unconditionally at module import time. This could cause issues if the user has already configured DSPy, or if OpenAI API keys are not available. Consider lazy initialization or checking if dspy.settings.lm is already set before overwriting it.
| # Ensure the LM is configured globally for DSPy | |
| if not dspy.settings.lm: | |
| dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) | |
| def _configure_default_dspy_lm() -> None: | |
| """Safely configure a default DSPy LM if none is set. | |
| This is intentionally defensive: it must not raise at import time | |
| if DSPy settings are not initialized or if API credentials are missing. | |
| """ | |
| try: | |
| current_lm = getattr(dspy.settings, "lm", None) | |
| except Exception: | |
| # If DSPy settings are not accessible yet, avoid failing import. | |
| return | |
| if current_lm: | |
| # An LM is already configured; do not overwrite it. | |
| return | |
| try: | |
| dspy.configure(lm=dspy.LM("openai/gpt-4o-mini")) | |
| except Exception: | |
| # Failure to configure a default LM should not prevent module import. | |
| # Downstream code can configure DSPy explicitly if needed. | |
| return | |
| # Ensure the LM is configured globally for DSPy (best-effort, non-fatal) | |
| _configure_default_dspy_lm() |
| probe = self.dspy_module.predict( | ||
| conversation_history=context, | ||
| observations=json.dumps([]), | ||
| probe_type_preferences=json.dumps(self.probe_type_preferences), | ||
| current_strategy=self._strategy.value, | ||
| test_objective="Explore agent capabilities", | ||
| ) |
There was a problem hiding this comment.
The probe method is expected to return the generated probe text string, but run_session doesn't extract it properly from the dspy_module.predict result. The predict call likely returns a structured output, not a raw string. You should extract the probe field from the prediction object.
| probe = self.dspy_module.predict( | |
| conversation_history=context, | |
| observations=json.dumps([]), | |
| probe_type_preferences=json.dumps(self.probe_type_preferences), | |
| current_strategy=self._strategy.value, | |
| test_objective="Explore agent capabilities", | |
| ) | |
| prediction = self.dspy_module.predict( | |
| conversation_history=context, | |
| observations=json.dumps([]), | |
| probe_type_preferences=json.dumps(self.probe_type_preferences), | |
| current_strategy=self._strategy.value, | |
| test_objective="Explore agent capabilities", | |
| ) | |
| # DSPy modules typically return a structured prediction object; extract the probe text. | |
| if isinstance(prediction, dict): | |
| probe = prediction.get("probe", prediction) | |
| else: | |
| probe = getattr(prediction, "probe", prediction) |
| print(f"Turn {turn}: Generated probe: {probe}") # Debugging | ||
| try: | ||
| response = await self.adapter.send_probe(probe) | ||
| print(f"Turn {turn}: Received response: {response}") # Debugging | ||
| self._record_probe( | ||
| turn=turn, | ||
| probe=probe, | ||
| response=response, | ||
| probe_type=self._select_probe_type(), | ||
| status="success", | ||
| ) | ||
| # Check termination criteria after recording the probe | ||
| if self.termination_criteria and self.termination_criteria(response): | ||
| print(f"Turn {turn}: Termination criteria met. Ending session.") # Debugging | ||
| break | ||
| except Exception as e: | ||
| print(f"Turn {turn}: Exception occurred: {e}") # Debugging | ||
| self._record_probe( | ||
| turn=turn, | ||
| probe=probe, | ||
| response="", | ||
| probe_type=self._select_probe_type(), | ||
| status=f"failure: {str(e)}", | ||
| ) | ||
|
|
||
| # Update context for the next turn | ||
| context = f"{context} {response}" |
There was a problem hiding this comment.
Debug print statements should be removed or converted to proper logging. These statements will clutter production output and should use the logging module instead.
| print(f"MockAdapter: Turn {self.turn_count}, Received probe: {probe}") # Debugging | ||
| if self.turn_count == 2: | ||
| print("MockAdapter: Simulating failure on turn 2") # Debugging | ||
| raise Exception("Simulated failure on turn 2") | ||
| if "STOP" in probe: | ||
| print("MockAdapter: Returning STOP response") # Debugging |
There was a problem hiding this comment.
Debug print statements should be removed or converted to proper logging before production. These print statements will appear in test output unnecessarily.
| print(f"MockAdapter: Turn {self.turn_count}, Received probe: {probe}") # Debugging | |
| if self.turn_count == 2: | |
| print("MockAdapter: Simulating failure on turn 2") # Debugging | |
| raise Exception("Simulated failure on turn 2") | |
| if "STOP" in probe: | |
| print("MockAdapter: Returning STOP response") # Debugging | |
| if self.turn_count == 2: | |
| raise Exception("Simulated failure on turn 2") | |
| if "STOP" in probe: |
| import json | ||
|
|
There was a problem hiding this comment.
This import of module json is redundant, as it was previously imported on line 10.
| import json |
| http_adapter = HTTPAdapter( | ||
| base_url=url, |
There was a problem hiding this comment.
Keyword argument 'base_url' is not a supported parameter name of HTTPAdapter.init.
| http_adapter = HTTPAdapter( | |
| base_url=url, | |
| # Configure the base URL on the adapter config to avoid passing | |
| # unsupported keyword arguments to HTTPAdapter.__init__. | |
| adapter_config.base_url = url | |
| http_adapter = HTTPAdapter( |
- Fix type annotations in optimizer.py for Callable signatures - Fix HTTPAdapterConfig import and usage in redteam/runner.py - Add proper type hints to investigator.py for ProbeHistory and run_session - Fix MockAdapter in tests to implement TargetAdapter protocol (send_message) - Add case-insensitive matching in _vuln_to_category for redteam adapter - Handle DSPy scenarios with empty assertions in test_designer.py - Update cli.py to use correct CoverageTracker and MockResponse APIs Resolves 47 mypy type errors and test failures. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused `cast` import from adapter.py - Remove unused `AdapterConfig` import from runner.py - Prefix unused `history` param with underscore in adapter.py - Prefix unused `extractor` variable with underscore in cli.py Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add use_dspy check in run_session() to use simple fallback probe generation when DSPy is disabled (avoids requiring API keys in CI) - Update test to use renamed _min_score parameter Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
Implements SARIF output for lint command and improves redteam module test coverage.
Changes
SARIF Output (Closes #35)
create_sarif_from_issues()to CLIlint --format sarifRedteam Coverage
TestDeepTeamUnavailable: 3 testsTestConfigYAMLLoading: 2 testsTestRunnerYAMLAndHTTP: 3 testsTestMockAdapterEdgeCases: 2 testsCoverage
config.pyrunner.pyTesting
quaestor lint --format sarifoutputs valid SARIF 2.1.0 JSON