Skip to content

Commit 63309e2

Browse files
a-orenclaude
andcommitted
feat(api): add TRUSTIFY_DA_RECOMMEND env var to disable recommendations
Aligns with the JavaScript client by allowing users to set TRUSTIFY_DA_RECOMMEND=false to append ?recommend=false to analysis URLs, disabling Trusted Content recommendations in responses. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent bf0257f commit 63309e2

2 files changed

Lines changed: 108 additions & 5 deletions

File tree

src/main/java/io/github/guacsec/trustifyda/impl/ExhortApi.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public final class ExhortApi implements Api {
101101
public static final String S_API_V5_LICENSES = "%s/api/v5/licenses/%s";
102102
public static final String S_API_V5_LICENSES_IDENTIFY = "%s/api/v5/licenses/identify";
103103
private static final String TRUSTIFY_DA_LICENSE_CHECK = "TRUSTIFY_DA_LICENSE_CHECK";
104+
private static final String TRUSTIFY_DA_RECOMMEND = "TRUSTIFY_DA_RECOMMEND";
104105

105106
private String endpoint;
106107

@@ -361,13 +362,21 @@ public static boolean debugLoggingIsNeeded() {
361362
return Environment.getBoolean("TRUSTIFY_DA_DEBUG", false);
362363
}
363364

365+
private static URI buildAnalysisUri(String template, String endpoint) {
366+
String base = String.format(template, endpoint);
367+
if (!Environment.getBoolean(TRUSTIFY_DA_RECOMMEND, true)) {
368+
return URI.create(base + "?recommend=false");
369+
}
370+
return URI.create(base);
371+
}
372+
364373
@Override
365374
public CompletableFuture<AnalysisReport> componentAnalysis(
366375
final String manifest, final byte[] manifestContent) throws IOException {
367376
String exClientTraceId = commonHookBeginning(false);
368377
var manifestPath = Path.of(manifest);
369378
var provider = Ecosystem.getProvider(manifestPath);
370-
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, getEndpoint()));
379+
var uri = buildAnalysisUri(S_API_V_5_ANALYSIS, getEndpoint());
371380
var content = provider.provideComponent();
372381
commonHookAfterProviderCreatedSbomAndBeforeExhort();
373382
return getAnalysisReportForComponent(uri, content, exClientTraceId);
@@ -411,7 +420,7 @@ public CompletableFuture<AnalysisReport> componentAnalysis(String manifestFile)
411420
String exClientTraceId = commonHookBeginning(false);
412421
var manifestPath = Path.of(manifestFile);
413422
var provider = Ecosystem.getProvider(manifestPath);
414-
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, getEndpoint()));
423+
var uri = buildAnalysisUri(S_API_V_5_ANALYSIS, getEndpoint());
415424
var content = provider.provideComponent();
416425
commonHookAfterProviderCreatedSbomAndBeforeExhort();
417426
return getAnalysisReportForComponent(uri, content, exClientTraceId);
@@ -452,7 +461,7 @@ private HttpRequest buildStackRequest(final String manifestFile, final MediaType
452461
throws IOException {
453462
var manifestPath = Path.of(manifestFile);
454463
var provider = Ecosystem.getProvider(manifestPath);
455-
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, getEndpoint()));
464+
var uri = buildAnalysisUri(S_API_V_5_ANALYSIS, getEndpoint());
456465
var content = provider.provideStack();
457466
commonHookAfterProviderCreatedSbomAndBeforeExhort();
458467

@@ -539,7 +548,7 @@ <H, T> CompletableFuture<T> performBatchAnalysis(
539548
final String analysisName)
540549
throws IOException {
541550
String exClientTraceId = commonHookBeginning(false);
542-
var uri = URI.create(String.format(S_API_V_5_BATCH_ANALYSIS, getEndpoint()));
551+
var uri = buildAnalysisUri(S_API_V_5_BATCH_ANALYSIS, getEndpoint());
543552
var sboms = sbomsGenerator.get();
544553
var content =
545554
new Provider.Content(
@@ -601,7 +610,7 @@ public CompletableFuture<ComponentAnalysisResult> componentAnalysisWithLicense(
601610
String exClientTraceId = commonHookBeginning(false);
602611
var manifestPath = Path.of(manifestFile);
603612
var provider = Ecosystem.getProvider(manifestPath);
604-
var uri = URI.create(String.format(S_API_V_5_ANALYSIS, getEndpoint()));
613+
var uri = buildAnalysisUri(S_API_V_5_ANALYSIS, getEndpoint());
605614
var content = provider.provideComponent();
606615
String sbomJson = new String(content.buffer);
607616
commonHookAfterProviderCreatedSbomAndBeforeExhort();

src/test/java/io/github/guacsec/trustifyda/impl/Exhort_Api_Test.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,100 @@ void generateSbom_should_contain_metadata_component() throws IOException {
896896
Files.deleteIfExists(tmpFile);
897897
}
898898

899+
@Test
900+
@SetSystemProperty(key = "TRUSTIFY_DA_RECOMMEND", value = "false")
901+
@SetSystemProperty(key = "TRUST_DA_TOKEN", value = "trust-da-token")
902+
@SetSystemProperty(key = "TRUST_DA_SOURCE", value = "trust-da-source")
903+
void stackAnalysis_when_recommend_disabled_should_append_query_param()
904+
throws IOException, ExecutionException, InterruptedException {
905+
var tmpFile = Files.createTempFile("TRUSTIFY_DA_test_pom_", ".xml");
906+
try (var is =
907+
getResourceAsStreamDecision(this.getClass(), "tst_manifests/maven/empty/pom.xml")) {
908+
Files.write(tmpFile, is.readAllBytes());
909+
}
910+
911+
given(mockProvider.provideStack())
912+
.willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type"));
913+
914+
ArgumentMatcher<HttpRequest> matchesRequest =
915+
r ->
916+
r.uri().toString().contains("?recommend=false")
917+
&& r.uri().toString().startsWith(exhortApiSut.getEndpoint() + "/api/v5/analysis")
918+
&& r.method().equals("POST");
919+
920+
var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
921+
AnalysisReport expectedAnalysis;
922+
try (var is =
923+
getResourceAsStreamDecision(
924+
this.getClass(), "dummy_responses/maven/analysis-report.json")) {
925+
expectedAnalysis = mapper.readValue(is, AnalysisReport.class);
926+
}
927+
928+
var mockHttpResponse = mock(HttpResponse.class);
929+
given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedAnalysis));
930+
given(mockHttpResponse.statusCode()).willReturn(200);
931+
932+
try (var ecosystemTool = mockStatic(Ecosystem.class)) {
933+
ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider);
934+
935+
given(mockHttpClient.sendAsync(argThat(matchesRequest), any()))
936+
.willReturn(CompletableFuture.completedFuture(mockHttpResponse));
937+
938+
var responseAnalysis = exhortApiSut.stackAnalysis(tmpFile.toString());
939+
then(responseAnalysis.get()).isEqualTo(expectedAnalysis);
940+
}
941+
Files.deleteIfExists(tmpFile);
942+
}
943+
944+
@Test
945+
@ClearSystemProperty(key = "TRUSTIFY_DA_RECOMMEND")
946+
@SetSystemProperty(key = "TRUST_DA_TOKEN", value = "trust-da-token")
947+
@SetSystemProperty(key = "TRUST_DA_SOURCE", value = "trust-da-source")
948+
void stackAnalysis_when_recommend_default_should_not_append_query_param()
949+
throws IOException, ExecutionException, InterruptedException {
950+
var tmpFile = Files.createTempFile("TRUSTIFY_DA_test_pom_", ".xml");
951+
try (var is =
952+
getResourceAsStreamDecision(this.getClass(), "tst_manifests/maven/empty/pom.xml")) {
953+
Files.write(tmpFile, is.readAllBytes());
954+
}
955+
956+
given(mockProvider.provideStack())
957+
.willReturn(new Provider.Content("fake-body-content".getBytes(), "fake-content-type"));
958+
959+
ArgumentMatcher<HttpRequest> matchesRequest =
960+
r ->
961+
!r.uri().toString().contains("recommend")
962+
&& r.uri()
963+
.equals(
964+
URI.create(
965+
String.format(
966+
ExhortApi.S_API_V_5_ANALYSIS, exhortApiSut.getEndpoint())))
967+
&& r.method().equals("POST");
968+
969+
var mapper = new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
970+
AnalysisReport expectedAnalysis;
971+
try (var is =
972+
getResourceAsStreamDecision(
973+
this.getClass(), "dummy_responses/maven/analysis-report.json")) {
974+
expectedAnalysis = mapper.readValue(is, AnalysisReport.class);
975+
}
976+
977+
var mockHttpResponse = mock(HttpResponse.class);
978+
given(mockHttpResponse.body()).willReturn(mapper.writeValueAsString(expectedAnalysis));
979+
given(mockHttpResponse.statusCode()).willReturn(200);
980+
981+
try (var ecosystemTool = mockStatic(Ecosystem.class)) {
982+
ecosystemTool.when(() -> Ecosystem.getProvider(tmpFile)).thenReturn(mockProvider);
983+
984+
given(mockHttpClient.sendAsync(argThat(matchesRequest), any()))
985+
.willReturn(CompletableFuture.completedFuture(mockHttpResponse));
986+
987+
var responseAnalysis = exhortApiSut.stackAnalysis(tmpFile.toString());
988+
then(responseAnalysis.get()).isEqualTo(expectedAnalysis);
989+
}
990+
Files.deleteIfExists(tmpFile);
991+
}
992+
899993
@Test
900994
void generateSbom_should_not_make_http_calls() throws IOException {
901995
var tmpFile = Files.createTempFile("TRUSTIFY_DA_test_pom_", ".xml");

0 commit comments

Comments
 (0)