diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8473ace5..748ccb29 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @ChrisEdwards @Contrast-Security-OSS/aiml-developers +* @ChrisEdwards @Contrast-Security-OSS/platform-developers diff --git a/contrast-mcp-core/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkextension/data/App.java b/contrast-mcp-core/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkextension/data/App.java index 7b0c1fcb..89c2043a 100644 --- a/contrast-mcp-core/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkextension/data/App.java +++ b/contrast-mcp-core/src/main/java/com/contrast/labs/ai/mcp/contrast/sdkextension/data/App.java @@ -27,9 +27,6 @@ public class App { @SerializedName("app_id") private String appId; - @SerializedName("last_seen") - private long lastSeen; - @SerializedName("last_reset") private Object lastReset; diff --git a/contrast-mcp-core/src/main/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveTool.java b/contrast-mcp-core/src/main/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveTool.java index 5bf147c1..21c4d89b 100644 --- a/contrast-mcp-core/src/main/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveTool.java +++ b/contrast-mcp-core/src/main/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveTool.java @@ -66,8 +66,8 @@ public class ListApplicationsByCveTool extends SingleTool listApplicationsByCve( @ToolParam(description = "CVE identifier (e.g., CVE-2021-44228)") String cveId, @@ -117,8 +117,6 @@ protected CveData doExecute(ListApplicationsByCveParams params, NoticeCollector return cveData; } - noticeNeverObservedApps(apps, collector); - log.debug( "Found {} applications vulnerable to {}, enriching with class usage data", apps.size(), @@ -141,19 +139,6 @@ protected CveData doExecute(ListApplicationsByCveParams params, NoticeCollector return cveData; } - // TeamServer sends last_seen 0 for applications that have never reported agent activity, and - // App.lastSeen is a primitive long, so the zero sentinel always serializes (Jira AIML-1331). - private static void noticeNeverObservedApps(List apps, NoticeCollector collector) { - var neverObserved = - apps.stream().filter(app -> app.getLastSeen() == 0).map(App::getName).toList(); - if (!neverObserved.isEmpty()) { - collector.notice( - "lastSeen of 0 means the application has never been observed running, typically a" - + " static or SCA-only upload: " - + String.join(", ", neverObserved)); - } - } - private static void applyPreferredCvssSummary(Cve cve) { if (cve == null) { return; diff --git a/contrast-mcp-core/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveToolTest.java b/contrast-mcp-core/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveToolTest.java index 1c6273e4..f11605e3 100644 --- a/contrast-mcp-core/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveToolTest.java +++ b/contrast-mcp-core/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveToolTest.java @@ -41,6 +41,7 @@ import org.junit.jupiter.api.Test; import org.springframework.ai.chat.model.ToolContext; import org.springframework.ai.tool.annotation.Tool; +import org.springframework.ai.util.json.JsonParser; class ListApplicationsByCveToolTest { @@ -50,10 +51,6 @@ class ListApplicationsByCveToolTest { private static final String SECRET_BODY = "token=raw-token-value&apiKey=secret"; private static final String CVSS_V2_NOTICE = "score is omitted for CVEs with only CVSS v2 data; use severity and the cvssv2 metrics."; - private static final long LAST_SEEN_MILLIS = 1721000000000L; - private static final String NEVER_OBSERVED_NOTICE_PREFIX = - "lastSeen of 0 means the application has never been observed running, typically a static or" - + " SCA-only upload: "; private static final String CVSS_V3_CVE_RESPONSE = """ { @@ -258,6 +255,9 @@ void listApplicationsByCve_should_forward_tool_context_and_preserve_tool_name() assertThat(result.isSuccess()).isTrue(); assertThat(capturedContext.get()).isSameAs(toolContext); assertThat(method.getAnnotation(Tool.class).name()).isEqualTo("list_applications_by_cve"); + assertThat(method.getAnnotation(Tool.class).description()) + .contains("search_applications", "search_servers") + .doesNotContain("lastSeen and server status"); } @Test @@ -305,29 +305,19 @@ void listApplicationsByCve_should_warn_for_empty_apps_list() throws Exception { } @Test - void listApplicationsByCve_should_notice_never_observed_apps_when_lastSeen_is_zero() - throws Exception { - var neverObserved = app("StaticUpload", "app-static"); - neverObserved.setLastSeen(0); - var running = app("Orders", APP_ID); - var cveData = new CveData(); - cveData.setApps(List.of(neverObserved, running)); - cveData.setLibraries(List.of(vulnerableLibrary(LIBRARY_HASH))); - - when(contrastApiClient.getApplicationsByCve(eq(CVE_ID))).thenReturn(cveData); - when(contrastApiClient.getAllLibraries(eq("app-static"))).thenReturn(List.of()); - when(contrastApiClient.getAllLibraries(eq(APP_ID))).thenReturn(List.of()); - - var result = tool.listApplicationsByCve(CVE_ID, null); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.notices()).contains(NEVER_OBSERVED_NOTICE_PREFIX + "StaticUpload"); - } - - @Test - void listApplicationsByCve_should_not_notice_never_observed_apps_when_lastSeen_is_populated() - throws Exception { - var cveData = cveData(app("Orders", APP_ID), vulnerableLibrary(LIBRARY_HASH)); + void + listApplicationsByCve_should_omit_unreliable_last_seen_and_never_observed_notice_when_teamserver_sends_last_seen_zero() + throws Exception { + var cveData = + GsonFactory.create() + .fromJson( + """ + { + "apps": [{"name": "Orders", "app_id": "app-123", "last_seen": 0}], + "libraries": [] + } + """, + CveData.class); when(contrastApiClient.getApplicationsByCve(eq(CVE_ID))).thenReturn(cveData); when(contrastApiClient.getAllLibraries(eq(APP_ID))).thenReturn(List.of()); @@ -336,6 +326,7 @@ void listApplicationsByCve_should_not_notice_never_observed_apps_when_lastSeen_i assertThat(result.isSuccess()).isTrue(); assertThat(result.notices()).noneMatch(n -> n.contains("never been observed running")); + assertThat(JsonParser.toJson(result)).doesNotContain("lastSeen", "last_seen"); } @Test @@ -519,7 +510,6 @@ private static App app(String name, String appId) { var app = new App(); app.setName(name); app.setAppId(appId); - app.setLastSeen(LAST_SEEN_MILLIS); return app; } diff --git a/contrast-mcp-stdio-app/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveLocalParityTest.java b/contrast-mcp-stdio-app/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveLocalParityTest.java index c56eb102..2c3aa65a 100644 --- a/contrast-mcp-stdio-app/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveLocalParityTest.java +++ b/contrast-mcp-stdio-app/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveLocalParityTest.java @@ -42,6 +42,7 @@ import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.util.json.JsonParser; @ExtendWith(MockitoExtension.class) class ListApplicationsByCveLocalParityTest { @@ -94,6 +95,8 @@ void listApplicationsByCve_should_return_cve_data_on_success() throws IOExceptio assertThat(result.data()).isNotNull(); assertThat(result.data().getApps()).isNotEmpty(); assertThat(result.errors()).isEmpty(); + assertThat(result.notices()).noneMatch(n -> n.contains("never been observed running")); + assertThat(JsonParser.toJson(result)).doesNotContain("lastSeen", "last_seen"); verify(sdkExtension).getAppsForCVE(eq(TEST_ORG_ID), eq(TEST_CVE_ID)); mockedSDKHelper.verify( () -> SDKHelper.getLibsForID(eq(TEST_APP_ID), eq(TEST_ORG_ID), eq(sdkExtension))); diff --git a/contrast-mcp-stdio-app/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveToolIT.java b/contrast-mcp-stdio-app/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveToolIT.java index b9485927..934bec0f 100644 --- a/contrast-mcp-stdio-app/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveToolIT.java +++ b/contrast-mcp-stdio-app/src/test/java/com/contrast/labs/ai/mcp/contrast/tool/library/ListApplicationsByCveToolIT.java @@ -26,6 +26,7 @@ import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.springframework.ai.util.json.JsonParser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; @@ -283,12 +284,8 @@ void listApplicationsByCve_should_populate_app_fields() { .as("known-vulnerable CVE must return at least one impacted app") .isNotEmpty(); - // Every returned App must populate identity plus non-negative seen-timestamps. Enrichment - // counters are validated separately in the class-usage test. - // - // lastSeen==0 is a documented sentinel for "app has never been observed running" (common - // for SCA-only apps that are catalogued but have no runtime activity). Only enforce - // lastSeen >= firstSeen when lastSeen is populated. + // Every returned App must populate identity plus a non-negative first-seen timestamp. + // Enrichment counters are validated separately in the class-usage test. assertThat(result.data().getApps()) .as("every impacted app must populate identity and timestamp fields") .allSatisfy( @@ -298,14 +295,6 @@ void listApplicationsByCve_should_populate_app_fields() { assertThat(app.getFirstSeen()) .as("%s.firstSeen must be non-negative", app.getAppId()) .isNotNegative(); - assertThat(app.getLastSeen()) - .as("%s.lastSeen must be non-negative", app.getAppId()) - .isNotNegative(); - if (app.getLastSeen() > 0) { - assertThat(app.getLastSeen()) - .as("%s.lastSeen must not precede firstSeen when populated", app.getAppId()) - .isGreaterThanOrEqualTo(app.getFirstSeen()); - } }); // Identity must actually be populated for every app — a real test of the payload, not just @@ -314,6 +303,8 @@ void listApplicationsByCve_should_populate_app_fields() { assertThat(result.data().getApps()) .as("at least one impacted app must carry a populated firstSeen timestamp") .anyMatch(app -> app.getFirstSeen() > 0); + assertThat(result.notices()).noneMatch(n -> n.contains("never been observed running")); + assertThat(JsonParser.toJson(result)).doesNotContain("lastSeen", "last_seen"); } @Test