Skip to content

Commit af5e162

Browse files
ChrisEdwardsclaude
andcommitted
Rename ValidationErrorFields to ValidationErrorField (singular) and add comprehensive tests
- Renamed ValidationErrorFields class to ValidationErrorField to follow singular naming convention - Updated all references in Application.java and test files - Added comprehensive test suite for Application JSON parsing - Tests verify correct parsing of missingRequiredFields and validationErrorFields - Covers edge cases: empty arrays, missing fields, and various field structures 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent b4d530a commit af5e162

File tree

4 files changed

+241
-12
lines changed

4 files changed

+241
-12
lines changed

CLAUDE.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is an MCP (Model Context Protocol) server for Contrast Security that enables AI agents to access and analyze vulnerability data from Contrast's security platform. It serves as a bridge between Contrast Security's API and AI tools like Claude, enabling automated vulnerability remediation and security analysis.
8+
9+
## Build and Development Commands
10+
11+
### Building the Project
12+
- **Build**: `mvn clean install` or `./mvnw clean install`
13+
- **Test**: `mvn test` or `./mvnw test`
14+
- **Run locally**: `java -jar target/mcp-contrast-0.0.9.jar --CONTRAST_HOST_NAME=<host> --CONTRAST_API_KEY=<key> --CONTRAST_SERVICE_KEY=<key> --CONTRAST_USERNAME=<user> --CONTRAST_ORG_ID=<org>`
15+
16+
### Docker Commands
17+
- **Build Docker image**: `docker build -t mcp-contrast .`
18+
- **Run with Docker**: `docker run -e CONTRAST_HOST_NAME=<host> -e CONTRAST_API_KEY=<key> -e CONTRAST_SERVICE_KEY=<key> -e CONTRAST_USERNAME=<user> -e CONTRAST_ORG_ID=<org> -i --rm mcp-contrast:latest -t stdio`
19+
20+
### Requirements
21+
- Java 17+
22+
- Maven 3.6+ (or use included wrapper `./mvnw`)
23+
- Docker (optional, for containerized deployment)
24+
25+
## Architecture
26+
27+
### Core Components
28+
29+
**Main Application**: `McpContrastApplication.java` - Spring Boot application that registers MCP tools from all service classes.
30+
31+
**Service Layer**: Each service handles a specific aspect of Contrast Security data:
32+
- `AssessService` - Vulnerability analysis and trace data
33+
- `SastService` - Static application security testing data
34+
- `SCAService` - Software composition analysis (library vulnerabilities)
35+
- `ADRService` - Attack detection and response events
36+
- `RouteCoverageService` - Route coverage analysis
37+
- `PromptService` - AI prompt management
38+
39+
**SDK Extensions**: Located in `sdkexstension/` package, these extend the Contrast SDK with enhanced data models and helper methods for better AI integration.
40+
41+
**Data Models**: Comprehensive POJOs in `data/` package representing vulnerability information, library data, applications, and attack events.
42+
43+
**Hint System**: `hints/` package provides context-aware security guidance for vulnerability remediation.
44+
45+
### Configuration
46+
47+
The application uses Spring Boot configuration with the following key properties:
48+
- `spring.ai.mcp.server.name=mcp-contrast`
49+
- `spring.main.web-application-type=none` (CLI application, not web server)
50+
- `contrast.api.protocol=https` (configurable for local development)
51+
52+
Required environment variables/arguments:
53+
- `CONTRAST_HOST_NAME` - Contrast TeamServer URL
54+
- `CONTRAST_API_KEY` - API authentication key
55+
- `CONTRAST_SERVICE_KEY` - Service authentication key
56+
- `CONTRAST_USERNAME` - User account
57+
- `CONTRAST_ORG_ID` - Organization identifier
58+
59+
### Technology Stack
60+
61+
- **Framework**: Spring Boot 3.4.5 with Spring AI 1.0.0-RC1
62+
- **MCP Integration**: Spring AI MCP Server starter
63+
- **Contrast Integration**: Contrast SDK Java 3.4.2
64+
- **Testing**: JUnit 5
65+
- **Build Tool**: Maven with wrapper
66+
- **Packaging**: Executable JAR and Docker container
67+
68+
### Development Patterns
69+
70+
1. **MCP Tools**: Services expose methods via `@Tool` annotation for AI agent consumption
71+
2. **SDK Extension Pattern**: Enhanced data models extend base SDK classes with AI-friendly representations
72+
3. **Hint Generation**: Rule-based system provides contextual security guidance
73+
4. **Defensive Design**: All external API calls include error handling and logging
74+
75+
### Security Considerations
76+
77+
This codebase handles sensitive vulnerability data. The README contains critical warnings about data privacy when using with AI models. Never expose Contrast credentials or vulnerability data to untrusted AI services.
78+
79+
### Logging
80+
81+
- Default log location: `/tmp/mcp-contrast.log`
82+
- Debug logging: Add `--logging.level.root=DEBUG` to startup arguments
83+
- Console logging is minimal by design for MCP protocol compatibility

src/main/java/com/contrast/labs/ai/mcp/contrast/sdkexstension/data/application/Application.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ public class Application {
6565
private List<Metadata> metadataEntities;
6666

6767
@SerializedName("validationErrorFields")
68-
private List<ValidationErrorFields> validationErrorFields;
68+
private List<ValidationErrorField> validationErrorFields;
6969

7070
@SerializedName("missingRequiredFields")
71-
private List<ValidationErrorFields> missingRequiredFields;
71+
private List<ValidationErrorField> missingRequiredFields;
7272

7373
@SerializedName("protect")
7474
private Object protect;
@@ -260,19 +260,19 @@ public void setMetadataEntities(List<Metadata> metadataEntities) {
260260
this.metadataEntities = metadataEntities;
261261
}
262262

263-
public List<ValidationErrorFields> getValidationErrorFields() {
263+
public List<ValidationErrorField> getValidationErrorFields() {
264264
return validationErrorFields;
265265
}
266266

267-
public void setValidationErrorFields(List<ValidationErrorFields> validationErrorFields) {
267+
public void setValidationErrorFields(List<ValidationErrorField> validationErrorFields) {
268268
this.validationErrorFields = validationErrorFields;
269269
}
270270

271-
public List<ValidationErrorFields> getMissingRequiredFields() {
271+
public List<ValidationErrorField> getMissingRequiredFields() {
272272
return missingRequiredFields;
273273
}
274274

275-
public void setMissingRequiredFields(List<ValidationErrorFields> missingRequiredFields) {
275+
public void setMissingRequiredFields(List<ValidationErrorField> missingRequiredFields) {
276276
this.missingRequiredFields = missingRequiredFields;
277277
}
278278

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
import javax.validation.Validation;
66
import java.util.List;
77

8-
public class ValidationErrorFields {
8+
public class ValidationErrorField {
99

1010
/**
11-
* Represents ValidationErrorFields information for an application.
11+
* Represents ValidationErrorField information for an application.
1212
*/
1313

1414
/**
@@ -43,7 +43,7 @@ public class ValidationErrorFields {
4343
private boolean unique;
4444

4545
@SerializedName("subfields")
46-
private List<ValidationErrorFields> subfields;
46+
private List<ValidationErrorField> subfields;
4747

4848
@SerializedName("links")
4949
private List<String> links;
@@ -85,10 +85,10 @@ public boolean isUnique() {
8585
public void setUnique(boolean unique) {
8686
this.unique = unique;
8787
}
88-
public List<ValidationErrorFields> getSubfields() {
88+
public List<ValidationErrorField> getSubfields() {
8989
return subfields;
9090
}
91-
public void setSubfields(List<ValidationErrorFields> subfields) {
91+
public void setSubfields(List<ValidationErrorField> subfields) {
9292
this.subfields = subfields;
9393
}
9494
public List<String> getLinks() {
@@ -100,7 +100,7 @@ public void setLinks(List<String> links) {
100100

101101
@Override
102102
public String toString() {
103-
return "ValidationErrorFields{" +
103+
return "ValidationErrorField{" +
104104
"fieldId='" + fieldId + '\'' +
105105
", fieldType='" + fieldType + '\'' +
106106
", displayLabel='" + displayLabel + '\'' +
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.contrast.labs.ai.mcp.contrast;
2+
3+
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.application.Application;
4+
import com.contrast.labs.ai.mcp.contrast.sdkexstension.data.application.ValidationErrorField;
5+
import com.google.gson.Gson;
6+
import org.junit.jupiter.api.DisplayName;
7+
import org.junit.jupiter.api.Test;
8+
9+
import static org.junit.jupiter.api.Assertions.*;
10+
11+
/**
12+
* Test class for Application JSON parsing.
13+
* Tests the deserialization of Application objects from JSON, specifically
14+
* focusing on the missingRequiredFields structure that caused parsing issues.
15+
*/
16+
public class ApplicationJsonParsingTest {
17+
18+
private final Gson gson = new Gson();
19+
20+
@Test
21+
@DisplayName("Test Application JSON parsing with missingRequiredFields as objects")
22+
public void testApplicationParsingWithMissingRequiredFieldsObjects() {
23+
// JSON that matches what TeamServer API returns
24+
String applicationJson = """
25+
{
26+
"app_id": "test-app-123",
27+
"name": "Test Application",
28+
"status": "online",
29+
"language": "JAVA",
30+
"last_seen": 1692467436000,
31+
"size": 12345,
32+
"missingRequiredFields": [
33+
{
34+
"fieldId": "29",
35+
"fieldType": "STRING",
36+
"displayLabel": "Custom Name",
37+
"agentLabel": "customName",
38+
"required": true,
39+
"unique": false,
40+
"subfields": null,
41+
"links": []
42+
},
43+
{
44+
"fieldId": "30",
45+
"fieldType": "SELECT",
46+
"displayLabel": "Environment",
47+
"agentLabel": "environment",
48+
"required": true,
49+
"unique": false,
50+
"subfields": null,
51+
"links": []
52+
}
53+
],
54+
"validationErrorFields": [],
55+
"metadataEntities": [],
56+
"tags": [],
57+
"techs": []
58+
}
59+
""";
60+
61+
// This should not throw an exception with the fix
62+
Application application = gson.fromJson(applicationJson, Application.class);
63+
64+
// Verify basic fields
65+
assertNotNull(application, "Application should not be null");
66+
assertEquals("test-app-123", application.getAppId(), "App ID should match");
67+
assertEquals("Test Application", application.getName(), "App name should match");
68+
assertEquals("online", application.getStatus(), "Status should match");
69+
70+
// Verify missingRequiredFields parsing
71+
assertNotNull(application.getMissingRequiredFields(), "Missing required fields should not be null");
72+
assertEquals(2, application.getMissingRequiredFields().size(), "Should have 2 missing required fields");
73+
74+
// Verify first missing required field
75+
ValidationErrorField firstField = application.getMissingRequiredFields().get(0);
76+
assertEquals("29", firstField.getFieldId(), "First field ID should match");
77+
assertEquals("STRING", firstField.getFieldType(), "First field type should match");
78+
assertEquals("Custom Name", firstField.getDisplayLabel(), "First field display label should match");
79+
assertEquals("customName", firstField.getAgentLabel(), "First field agent label should match");
80+
assertTrue(firstField.isRequired(), "First field should be required");
81+
assertFalse(firstField.isUnique(), "First field should not be unique");
82+
83+
// Verify second missing required field
84+
ValidationErrorField secondField = application.getMissingRequiredFields().get(1);
85+
assertEquals("30", secondField.getFieldId(), "Second field ID should match");
86+
assertEquals("SELECT", secondField.getFieldType(), "Second field type should match");
87+
assertEquals("Environment", secondField.getDisplayLabel(), "Second field display label should match");
88+
assertEquals("environment", secondField.getAgentLabel(), "Second field agent label should match");
89+
assertTrue(secondField.isRequired(), "Second field should be required");
90+
assertFalse(secondField.isUnique(), "Second field should not be unique");
91+
}
92+
93+
@Test
94+
@DisplayName("Test Application JSON parsing with empty missingRequiredFields")
95+
public void testApplicationParsingWithEmptyMissingRequiredFields() {
96+
String applicationJson = """
97+
{
98+
"app_id": "test-app-456",
99+
"name": "Test Application 2",
100+
"status": "offline",
101+
"language": "JAVASCRIPT",
102+
"last_seen": 1692467436000,
103+
"size": 54321,
104+
"missingRequiredFields": [],
105+
"validationErrorFields": [],
106+
"metadataEntities": [],
107+
"tags": [],
108+
"techs": []
109+
}
110+
""";
111+
112+
Application application = gson.fromJson(applicationJson, Application.class);
113+
114+
assertNotNull(application, "Application should not be null");
115+
assertEquals("test-app-456", application.getAppId(), "App ID should match");
116+
assertNotNull(application.getMissingRequiredFields(), "Missing required fields should not be null");
117+
assertTrue(application.getMissingRequiredFields().isEmpty(), "Missing required fields should be empty");
118+
}
119+
120+
@Test
121+
@DisplayName("Test Application JSON parsing with null missingRequiredFields")
122+
public void testApplicationParsingWithNullMissingRequiredFields() {
123+
String applicationJson = """
124+
{
125+
"app_id": "test-app-789",
126+
"name": "Test Application 3",
127+
"status": "online",
128+
"language": "PYTHON",
129+
"last_seen": 1692467436000,
130+
"size": 98765,
131+
"validationErrorFields": [],
132+
"metadataEntities": [],
133+
"tags": [],
134+
"techs": []
135+
}
136+
""";
137+
138+
// Should handle missing missingRequiredFields field gracefully
139+
Application application = gson.fromJson(applicationJson, Application.class);
140+
141+
assertNotNull(application, "Application should not be null");
142+
assertEquals("test-app-789", application.getAppId(), "App ID should match");
143+
// Field should be null when not present in JSON
144+
assertNull(application.getMissingRequiredFields(), "Missing required fields should be null when not in JSON");
145+
}
146+
}

0 commit comments

Comments
 (0)