import org.sonar.api.utils.Paging;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
-import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.component.ComponentDto;
import static org.sonar.api.utils.Paging.forPageIndex;
import static org.sonar.core.util.stream.MoreCollectors.toList;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_INSECURE_INTERACTION;
+import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_POROUS_DEFENSES;
+import static org.sonar.server.security.SecurityStandards.SANS_TOP_25_RISKY_RESOURCE;
import static org.sonar.server.security.SecurityStandards.fromSecurityStandards;
import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001;
import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001;
private static final String PARAM_PULL_REQUEST = "pullRequest";
private static final String PARAM_SINCE_LEAK_PERIOD = "sinceLeakPeriod";
private static final String PARAM_ONLY_MINE = "onlyMine";
+ private static final String PARAM_OWASP_TOP_10 = "owaspTop10";
+ private static final String PARAM_SANS_TOP_25 = "sansTop25";
+ private static final String PARAM_SONARSOURCE_SECURITY = "sonarsourceSecurity";
+
private static final List<String> STATUSES = ImmutableList.of(STATUS_TO_REVIEW, STATUS_REVIEWED);
private final DbClient dbClient;
private final IssueIndex issueIndex;
private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker;
private final HotspotWsResponseFormatter responseFormatter;
- private System2 system2;
+ private final System2 system2;
public SearchAction(DbClient dbClient, UserSession userSession, IssueIndex issueIndex,
IssueIndexSyncProgressChecker issueIndexSyncProgressChecker,
.createAction("search")
.setHandler(this)
.setDescription("Search for Security Hotpots."
- + "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.")
+ + "<br/>When issue indexation is in progress returns 503 service unavailable HTTP code.")
.setSince("8.1")
.setInternal(true);
.setDescription("If 'projectKey' is provided, returns only Security Hotspots assigned to the current user")
.setBooleanPossibleValues()
.setRequired(false);
+ action.createParam(PARAM_OWASP_TOP_10)
+ .setDescription("Comma-separated list of OWASP Top 10 lowercase categories.")
+ .setSince("8.6")
+ .setPossibleValues("a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10");
+ action.createParam(PARAM_SANS_TOP_25)
+ .setDescription("Comma-separated list of SANS Top 25 categories.")
+ .setSince("8.6")
+ .setPossibleValues(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES);
+ action.createParam(PARAM_SONARSOURCE_SECURITY)
+ .setDescription("Comma-separated list of SonarSource security categories. Use '" + SecurityStandards.SQCategory.OTHERS.getKey() +
+ "' to select issues not associated with any category")
+ .setSince("8.6")
+ .setPossibleValues(Arrays.stream(SecurityStandards.SQCategory.values()).map(SecurityStandards.SQCategory::getKey).collect(Collectors.toList()));
action.setResponseExample(getClass().getResource("search-example.json"));
}
try (DbSession dbSession = dbClient.openSession(false)) {
checkIfNeedIssueSync(dbSession, wsRequest);
Optional<ComponentDto> project = getAndValidateProjectOrApplication(dbSession, wsRequest);
- SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project, wsRequest.getHotspotKeys());
+ SearchResponseData searchResponseData = searchHotspots(wsRequest, dbSession, project.orElse(null));
loadComponents(dbSession, searchResponseData);
loadRules(dbSession, searchResponseData);
writeProtobuf(formatResponse(searchResponseData), request, response);
}
private static WsRequest toWsRequest(Request request) {
- List<String> hotspotKeysList = request.paramAsStrings(PARAM_HOTSPOTS);
- Set<String> hotspotKeys = hotspotKeysList == null ? ImmutableSet.of() : hotspotKeysList.stream().collect(MoreCollectors.toSet(hotspotKeysList.size()));
+ List<String> hotspotList = request.paramAsStrings(PARAM_HOTSPOTS);
+ Set<String> hotspotKeys = hotspotList != null ? ImmutableSet.copyOf(hotspotList) : ImmutableSet.of();
+ List<String> owaspTop10List = request.paramAsStrings(PARAM_OWASP_TOP_10);
+ Set<String> owaspTop10 = owaspTop10List != null ? ImmutableSet.copyOf(owaspTop10List) : ImmutableSet.of();
+ List<String> sansTop25List = request.paramAsStrings(PARAM_SANS_TOP_25);
+ Set<String> sansTop25 = sansTop25List != null ? ImmutableSet.copyOf(sansTop25List) : ImmutableSet.of();
+ List<String> sonarsourceSecurityList = request.paramAsStrings(PARAM_SONARSOURCE_SECURITY);
+ Set<String> sonarsourceSecurity = sonarsourceSecurityList != null ? ImmutableSet.copyOf(sonarsourceSecurityList) : ImmutableSet.of();
+
return new WsRequest(
- request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE),
- request.param(PARAM_PROJECT_KEY), request.param(PARAM_BRANCH), request.param(PARAM_PULL_REQUEST),
- hotspotKeys,
- request.param(PARAM_STATUS), request.param(PARAM_RESOLUTION),
- request.paramAsBoolean(PARAM_SINCE_LEAK_PERIOD),
- request.paramAsBoolean(PARAM_ONLY_MINE));
+ request.mandatoryParamAsInt(PAGE), request.mandatoryParamAsInt(PAGE_SIZE), request.param(PARAM_PROJECT_KEY), request.param(PARAM_BRANCH),
+ request.param(PARAM_PULL_REQUEST), hotspotKeys, request.param(PARAM_STATUS), request.param(PARAM_RESOLUTION),
+ request.paramAsBoolean(PARAM_SINCE_LEAK_PERIOD), request.paramAsBoolean(PARAM_ONLY_MINE), owaspTop10, sansTop25, sonarsourceSecurity);
}
private void validateParameters(WsRequest wsRequest) {
private Optional<ComponentDto> getAndValidateProjectOrApplication(DbSession dbSession, WsRequest wsRequest) {
return wsRequest.getProjectKey().map(projectKey -> {
- ComponentDto project = getProject(dbSession, projectKey, wsRequest.getBranch(), wsRequest.getPullRequest())
+ ComponentDto project = getProject(dbSession, projectKey, wsRequest.getBranch().orElse(null), wsRequest.getPullRequest().orElse(null))
.filter(t -> Scopes.PROJECT.equals(t.scope()) && SUPPORTED_QUALIFIERS.contains(t.qualifier()))
.filter(ComponentDto::isEnabled)
.orElseThrow(() -> new NotFoundException(format("Project '%s' not found", projectKey)));
});
}
- private Optional<ComponentDto> getProject(DbSession dbSession, String projectKey, Optional<String> branch, Optional<String> pullRequest) {
- if (branch.isPresent()) {
- return dbClient.componentDao().selectByKeyAndBranch(dbSession, projectKey, branch.get());
- } else if (pullRequest.isPresent()) {
- return dbClient.componentDao().selectByKeyAndPullRequest(dbSession, projectKey, pullRequest.get());
+ private Optional<ComponentDto> getProject(DbSession dbSession, String projectKey, @Nullable String branch, @Nullable String pullRequest) {
+ if (branch != null) {
+ return dbClient.componentDao().selectByKeyAndBranch(dbSession, projectKey, branch);
+ } else if (pullRequest != null) {
+ return dbClient.componentDao().selectByKeyAndPullRequest(dbSession, projectKey, pullRequest);
}
return dbClient.componentDao().selectByKey(dbSession, projectKey);
}
- private SearchResponseData searchHotspots(WsRequest wsRequest, DbSession dbSession, Optional<ComponentDto> project, Set<String> hotspotKeys) {
- SearchResponse result = doIndexSearch(wsRequest, dbSession, project, hotspotKeys);
+ private SearchResponseData searchHotspots(WsRequest wsRequest, DbSession dbSession, @Nullable ComponentDto project) {
+ SearchResponse result = doIndexSearch(wsRequest, dbSession, project);
List<String> issueKeys = Arrays.stream(result.getHits().getHits())
.map(SearchHit::getId)
.collect(toList(result.getHits().getHits().length));
.collect(Collectors.toList());
}
- private SearchResponse doIndexSearch(WsRequest wsRequest, DbSession dbSession, Optional<ComponentDto> project, Set<String> hotspotKeys) {
+ private SearchResponse doIndexSearch(WsRequest wsRequest, DbSession dbSession, @Nullable ComponentDto project) {
IssueQuery.Builder builder = IssueQuery.builder()
.types(singleton(RuleType.SECURITY_HOTSPOT.name()))
.sort(IssueQuery.SORT_HOTSPOTS)
.asc(true)
.statuses(wsRequest.getStatus().map(Collections::singletonList).orElse(STATUSES));
- project.ifPresent(p -> {
- builder.organizationUuid(p.getOrganizationUuid());
- String projectUuid = firstNonNull(p.getMainBranchProjectUuid(), p.uuid());
- if (Qualifiers.APP.equals(p.qualifier())) {
+ if (project != null) {
+ builder.organizationUuid(project.getOrganizationUuid());
+ String projectUuid = firstNonNull(project.getMainBranchProjectUuid(), project.uuid());
+ if (Qualifiers.APP.equals(project.qualifier())) {
builder.viewUuids(singletonList(projectUuid));
} else {
builder.projectUuids(singletonList(projectUuid));
}
- if (p.getMainBranchProjectUuid() == null) {
+ if (project.getMainBranchProjectUuid() == null) {
builder.mainBranch(true);
} else {
- builder.branchUuid(p.uuid());
+ builder.branchUuid(project.uuid());
builder.mainBranch(false);
}
if (wsRequest.isSinceLeakPeriod() && !wsRequest.getPullRequest().isPresent()) {
- Date sinceDate = dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, p.uuid())
+ Date sinceDate = dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, project.uuid())
.map(s -> longToDate(s.getPeriodDate()))
.orElseGet(() -> new Date(system2.now()));
builder.createdAfter(sinceDate, false);
}
- });
- if (!hotspotKeys.isEmpty()) {
- builder.issueKeys(hotspotKeys);
+ }
+
+ if (!wsRequest.getHotspotKeys().isEmpty()) {
+ builder.issueKeys(wsRequest.getHotspotKeys());
}
if (wsRequest.isOnlyMine()) {
wsRequest.getStatus().ifPresent(status -> builder.resolved(STATUS_REVIEWED.equals(status)));
wsRequest.getResolution().ifPresent(resolution -> builder.resolutions(singleton(resolution)));
+ if (!wsRequest.getOwaspTop10().isEmpty()) {
+ builder.owaspTop10(wsRequest.getOwaspTop10());
+ }
+ if (!wsRequest.getSansTop25().isEmpty()) {
+ builder.sansTop25(wsRequest.getSansTop25());
+ }
+ if (!wsRequest.getSonarsourceSecurity().isEmpty()) {
+ builder.sonarsourceSecurity(wsRequest.getSonarsourceSecurity());
+ }
IssueQuery query = builder.build();
SearchOptions searchOptions = new SearchOptions()
private final String resolution;
private final boolean sinceLeakPeriod;
private final boolean onlyMine;
+ private final Set<String> owaspTop10;
+ private final Set<String> sansTop25;
+ private final Set<String> sonarsourceSecurity;
private WsRequest(int page, int index,
@Nullable String projectKey, @Nullable String branch, @Nullable String pullRequest,
Set<String> hotspotKeys,
@Nullable String status, @Nullable String resolution, @Nullable Boolean sinceLeakPeriod,
- @Nullable Boolean onlyMine) {
+ @Nullable Boolean onlyMine, Set<String> owaspTop10, Set<String> sansTop25, Set<String> sonarsourceSecurity) {
this.page = page;
this.index = index;
this.projectKey = projectKey;
this.resolution = resolution;
this.sinceLeakPeriod = sinceLeakPeriod != null && sinceLeakPeriod;
this.onlyMine = onlyMine != null && onlyMine;
+ this.owaspTop10 = owaspTop10;
+ this.sansTop25 = sansTop25;
+ this.sonarsourceSecurity = sonarsourceSecurity;
}
int getPage() {
boolean isOnlyMine() {
return onlyMine;
}
+
+ public Set<String> getOwaspTop10() {
+ return owaspTop10;
+ }
+
+ public Set<String> getSansTop25() {
+ return sansTop25;
+ }
+
+ public Set<String> getSonarsourceSecurity() {
+ return sonarsourceSecurity;
+ }
}
private static final class SearchResponseData {
*/
package org.sonar.server.hotspot.ws;
-import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.tngtech.java.junit.dataprovider.DataProvider;
import org.sonar.api.impl.utils.TestSystem2;
import org.sonar.api.issue.Issue;
import org.sonar.api.rules.RuleType;
+import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.db.DbClient;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.issue.index.AsyncIssueIndexing;
import org.sonar.server.issue.index.IssueIndex;
import org.sonar.server.issue.index.IssueIndexSyncProgressChecker;
import org.sonar.server.issue.index.IssueIndexer;
import org.sonarqube.ws.Hotspots.Component;
import org.sonarqube.ws.Hotspots.SearchWsResponse;
+import static com.google.common.collect.ImmutableSet.of;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@Rule
public UserSessionRule userSessionRule = UserSessionRule.standalone();
- private TestSystem2 system2 = new TestSystem2();
- private DbClient dbClient = dbTester.getDbClient();
- private TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(dbTester);
-
- private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule));
- private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), null);
- private ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client());
- private PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
- private HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider);
- private IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
- private SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex,
+ private final TestSystem2 system2 = new TestSystem2();
+ private final DbClient dbClient = dbTester.getDbClient();
+ private final TestDefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(dbTester);
+ private final IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule));
+ private final IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient), mock(AsyncIssueIndexing.class));
+ private final ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client());
+ private final PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
+ private final HotspotWsResponseFormatter responseFormatter = new HotspotWsResponseFormatter(defaultOrganizationProvider);
+ private final IssueIndexSyncProgressChecker issueIndexSyncProgressChecker = mock(IssueIndexSyncProgressChecker.class);
+ private final SearchAction underTest = new SearchAction(dbClient, userSessionRule, issueIndex,
issueIndexSyncProgressChecker, responseFormatter, system2);
- private WsActionTester actionTester = new WsActionTester(underTest);
+ private final WsActionTester actionTester = new WsActionTester(underTest);
@Test
public void verify_ws_def() {
+ WebService.Param onlyMineParam = actionTester.getDef().param("onlyMine");
+ WebService.Param owaspTop10Param = actionTester.getDef().param("owaspTop10");
+ WebService.Param sansTop25Param = actionTester.getDef().param("sansTop25");
+ WebService.Param sonarsourceSecurityParam = actionTester.getDef().param("sonarsourceSecurity");
+
assertThat(actionTester.getDef().isInternal()).isTrue();
- assertThat(actionTester.getDef().param("onlyMine").isRequired()).isFalse();
+ assertThat(onlyMineParam).isNotNull();
+ assertThat(onlyMineParam.isRequired()).isFalse();
assertThat(actionTester.getDef().param("onlyMine").possibleValues())
.containsExactlyInAnyOrder("yes", "no", "true", "false");
+
+ assertThat(owaspTop10Param).isNotNull();
+ assertThat(owaspTop10Param.isRequired()).isFalse();
+ assertThat(sansTop25Param).isNotNull();
+ assertThat(sansTop25Param.isRequired()).isFalse();
+ assertThat(sonarsourceSecurityParam).isNotNull();
+ assertThat(sonarsourceSecurityParam.isRequired()).isFalse();
}
@Test
userSessionRule.registerComponents(project);
userSessionRule.logIn().addProjectPermission(UserRole.USER, project);
- assertThatThrownBy(() -> actionTester.newRequest()
+ TestRequest request = actionTester.newRequest()
.setParam("hotspots", IntStream.range(2, 10).mapToObj(String::valueOf).collect(joining(",")))
- .setParam("onlyMine", "true")
- .execute())
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Parameter 'onlyMine' can be used with parameter 'projectKey' only");
+ .setParam("onlyMine", "true");
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Parameter 'onlyMine' can be used with parameter 'projectKey' only");
}
@Test
userSessionRule.anonymous();
- assertThatThrownBy(() -> actionTester.newRequest()
+ TestRequest request = actionTester.newRequest()
.setParam("projectKey", project.getKey())
- .setParam("onlyMine", "true")
- .execute())
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Parameter 'onlyMine' requires user to be logged in");
+ .setParam("onlyMine", "true");
+ assertThatThrownBy(request::execute)
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage("Parameter 'onlyMine' requires user to be logged in");
}
@Test
});
Stream<Object[]> sqCategoryOTHERS = Stream.of(
new Object[] {Collections.emptySet(), SQCategory.OTHERS},
- new Object[] {ImmutableSet.of("foo", "donut", "acme"), SQCategory.OTHERS});
+ new Object[] {of("foo", "donut", "acme"), SQCategory.OTHERS});
return Stream.concat(allCategoriesButOTHERS, sqCategoryOTHERS).toArray(Object[][]::new);
}
.containsExactlyInAnyOrder(selectedHotspots.stream().map(IssueDto::getKey).toArray(String[]::new));
}
+ @Test
+ public void returns_hotspots_with_specified_sonarsourceSecurity_category() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule1 = newRule(SECURITY_HOTSPOT);
+ RuleDefinitionDto rule2 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("cwe:117", "cwe:190")));
+ RuleDefinitionDto rule3 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("owaspTop10:a1", "cwe:601")));
+ insertHotspot(project, file, rule1);
+ IssueDto hotspot2 = insertHotspot(project, file, rule2);
+ insertHotspot(project, file, rule3);
+ indexIssues();
+
+ SearchWsResponse response = newRequest(project).setParam("sonarsourceSecurity", "log-injection")
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .extracting(SearchWsResponse.Hotspot::getKey)
+ .containsExactly(hotspot2.getKey());
+ }
+
+ @Test
+ public void returns_hotspots_with_specified_owaspTop10_category() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule1 = newRule(SECURITY_HOTSPOT);
+ RuleDefinitionDto rule2 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("cwe:117", "cwe:190")));
+ RuleDefinitionDto rule3 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("owaspTop10:a1", "cwe:601")));
+ insertHotspot(project, file, rule1);
+ insertHotspot(project, file, rule2);
+ IssueDto hotspot3 = insertHotspot(project, file, rule3);
+ indexIssues();
+
+ SearchWsResponse response = newRequest(project).setParam("owaspTop10", "a1")
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .extracting(SearchWsResponse.Hotspot::getKey)
+ .containsExactly(hotspot3.getKey());
+ }
+
+ @Test
+ public void returns_hotspots_with_specified_sansTop25_category() {
+ ComponentDto project = dbTester.components().insertPublicProject();
+ userSessionRule.registerComponents(project);
+ indexPermissions();
+ ComponentDto file = dbTester.components().insertComponent(newFileDto(project));
+ RuleDefinitionDto rule1 = newRule(SECURITY_HOTSPOT);
+ RuleDefinitionDto rule2 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("cwe:117", "cwe:190")));
+ RuleDefinitionDto rule3 = newRule(SECURITY_HOTSPOT, r -> r.setSecurityStandards(of("owaspTop10:a1", "cwe:601")));
+ insertHotspot(project, file, rule1);
+ insertHotspot(project, file, rule2);
+ IssueDto hotspot3 = insertHotspot(project, file, rule3);
+ indexIssues();
+
+ SearchWsResponse response = newRequest(project).setParam("sansTop25", "insecure-interaction")
+ .executeProtobuf(SearchWsResponse.class);
+
+ assertThat(response.getHotspotsList())
+ .extracting(SearchWsResponse.Hotspot::getKey)
+ .containsExactly(hotspot3.getKey());
+ }
+
@Test
public void returns_hotspots_on_the_leak_period_when_sinceLeakPeriod_is_true() {
ComponentDto project = dbTester.components().insertPublicProject();
}
@Test
- public void returnsall_issues_when_sinceLeakPeriod_is_true_and_is_pr() {
+ public void returns_all_issues_when_sinceLeakPeriod_is_true_and_is_pr() {
long referenceDate = 800_996_999_332L;
system2.setNow(referenceDate + 10_000);