Skip to content

Commit 668bfd4

Browse files
authored
Merge pull request #11 from Contrast-Security-OSS/AIML-89
Fix JSON parsing error for missingRequiredFields
2 parents b938521 + 5229242 commit 668bfd4

File tree

4 files changed

+241
-14
lines changed

4 files changed

+241
-14
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 & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import com.google.gson.annotations.SerializedName;
44

5-
import javax.validation.Validation;
65
import java.util.List;
76

87
/**
@@ -65,10 +64,10 @@ public class Application {
6564
private List<Metadata> metadataEntities;
6665

6766
@SerializedName("validationErrorFields")
68-
private List<ValidationErrorFields> validationErrorFields;
67+
private List<Field> validationErrorFields;
6968

7069
@SerializedName("missingRequiredFields")
71-
private List<String> missingRequiredFields;
70+
private List<Field> missingRequiredFields;
7271

7372
@SerializedName("protect")
7473
private Object protect;
@@ -260,19 +259,19 @@ public void setMetadataEntities(List<Metadata> metadataEntities) {
260259
this.metadataEntities = metadataEntities;
261260
}
262261

263-
public List<ValidationErrorFields> getValidationErrorFields() {
262+
public List<Field> getValidationErrorFields() {
264263
return validationErrorFields;
265264
}
266265

267-
public void setValidationErrorFields(List<ValidationErrorFields> validationErrorFields) {
266+
public void setValidationErrorFields(List<Field> validationErrorFields) {
268267
this.validationErrorFields = validationErrorFields;
269268
}
270269

271-
public List<String> getMissingRequiredFields() {
270+
public List<Field> getMissingRequiredFields() {
272271
return missingRequiredFields;
273272
}
274273

275-
public void setMissingRequiredFields(List<String> missingRequiredFields) {
274+
public void setMissingRequiredFields(List<Field> missingRequiredFields) {
276275
this.missingRequiredFields = missingRequiredFields;
277276
}
278277

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33
import com.google.gson.annotations.SerializedName;
44

5-
import javax.validation.Validation;
65
import java.util.List;
76

8-
public class ValidationErrorFields {
7+
public class Field {
98

109
/**
11-
* Represents ValidationErrorFields information for an application.
10+
* Represents Field information for an application.
1211
*/
1312

1413
/**
@@ -43,7 +42,7 @@ public class ValidationErrorFields {
4342
private boolean unique;
4443

4544
@SerializedName("subfields")
46-
private List<ValidationErrorFields> subfields;
45+
private List<Field> subfields;
4746

4847
@SerializedName("links")
4948
private List<String> links;
@@ -85,10 +84,10 @@ public boolean isUnique() {
8584
public void setUnique(boolean unique) {
8685
this.unique = unique;
8786
}
88-
public List<ValidationErrorFields> getSubfields() {
87+
public List<Field> getSubfields() {
8988
return subfields;
9089
}
91-
public void setSubfields(List<ValidationErrorFields> subfields) {
90+
public void setSubfields(List<Field> subfields) {
9291
this.subfields = subfields;
9392
}
9493
public List<String> getLinks() {
@@ -100,7 +99,7 @@ public void setLinks(List<String> links) {
10099

101100
@Override
102101
public String toString() {
103-
return "ValidationErrorFields{" +
102+
return "Field{" +
104103
"fieldId='" + fieldId + '\'' +
105104
", fieldType='" + fieldType + '\'' +
106105
", 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.Field;
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+
Field 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+
Field 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)