Skip to content

Commit f5ac7db

Browse files
authored
Merge pull request #142 from snyk/feat/SLS-709-created-delay-download
feat: Add handling for a optional configurable delay in days to prevent package downloads
2 parents e4fa4fb + 375ca59 commit f5ac7db

8 files changed

Lines changed: 159 additions & 13 deletions

File tree

.snyk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ ignore:
4646
'SNYK-JAVA-ORGGLASSFISHJERSEYCORE-14049172':
4747
- '*':
4848
reason: Transitive dep of artifactory-papi. Actual papi is provided by artifactory env at runtime so this is a false positive.
49-
created: 2025-11-19T00:00:00.000Z
49+
created: 2025-11-19T00:00:00.000Z

core/src/main/groovy/io/snyk/plugins/artifactory/snykSecurityPlugin.properties

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ snyk.api.organization=
6666
# Default: 24 hours (1 day)
6767
#snyk.scanner.extendTestDeadline.hours=24
6868

69+
# A delay in number of days since the package was last modified in Artifactory. Any packages that were modified more recently
70+
# than the current time minus the number of days in this configuration will be blocked from download. The use case is to prevent
71+
# packages that may contain zero-day vulnerabilities from being introduced to a consumer.
72+
# Default: 0
73+
#snyk.scanner.lastModified.days=0
74+
75+
# If remoteOnly is set to true, only check lastModified for packages contained in remote repositories.
76+
# Default: true
77+
#snyk.scanner.lastModified.remoteOnly=true
78+
6979
# By default, if Snyk API fails while scanning an artifact for any reason, the download will be allowed.
7080
# Setting this property to "true" will block downloads when Snyk API fails.
7181
# Accepts: "true", "false"

core/src/main/java/io/snyk/plugins/artifactory/configuration/PluginConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public enum PluginConfiguration implements Configuration {
3232
SCANNER_PACKAGE_TYPE_COCOAPODS("snyk.scanner.packageType.cocoapods", "false"),
3333
TEST_CONTINUOUSLY("snyk.scanner.test.continuously","false"),
3434
TEST_FREQUENCY_HOURS("snyk.scanner.frequency.hours", "168"),
35-
EXTEND_TEST_DEADLINE_HOURS("snyk.scanner.extendTestDeadline.hours", "24");
35+
EXTEND_TEST_DEADLINE_HOURS("snyk.scanner.extendTestDeadline.hours", "24"),
36+
SCANNER_LAST_MODIFIED_DELAY_DAYS("snyk.scanner.lastModified.days", "0"),
37+
SCANNER_LAST_MODIFIED_CHECK_ONLY_REMOTE("snyk.scanner.lastModified.remoteOnly", "true");
3638

3739
private final String propertyKey;
3840
private final String defaultValue;

core/src/main/java/io/snyk/plugins/artifactory/model/MonitoredArtifact.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import static io.snyk.plugins.artifactory.configuration.properties.ArtifactProperty.*;
1111
import static org.slf4j.LoggerFactory.getLogger;
1212

13+
import java.time.Instant;
14+
1315
public class MonitoredArtifact {
1416

1517
private static final Logger LOG = getLogger(MonitoredArtifact.class);
@@ -20,10 +22,17 @@ public class MonitoredArtifact {
2022

2123
private final Ignores ignores;
2224

25+
private final Instant lastModifiedDate;
26+
2327
public MonitoredArtifact(String path, TestResult testResult, Ignores ignores) {
28+
this(path, testResult, ignores, null);
29+
}
30+
31+
public MonitoredArtifact(String path, TestResult testResult, Ignores ignores, Instant lastModifiedDate) {
2432
this.path = path;
2533
this.testResult = testResult;
2634
this.ignores = ignores;
35+
this.lastModifiedDate = lastModifiedDate;
2736
}
2837

2938
public String getPath() {
@@ -38,6 +47,10 @@ public Ignores getIgnores() {
3847
return ignores;
3948
}
4049

50+
public Optional<Instant> getLastModifiedDate() {
51+
return Optional.ofNullable(lastModifiedDate);
52+
}
53+
4154
public MonitoredArtifact write(ArtifactProperties properties) {
4255
testResult.write(properties);
4356

@@ -61,7 +74,8 @@ public static Optional<MonitoredArtifact> read(ArtifactProperties properties) {
6174
new MonitoredArtifact(
6275
properties.getArtifactPath(),
6376
testResult,
64-
Ignores.read(properties)
77+
Ignores.read(properties),
78+
null // Created date not stored in properties
6579
)
6680
);
6781
} catch (RuntimeException e) {
@@ -75,12 +89,12 @@ public boolean equals(Object o) {
7589
if (this == o) return true;
7690
if (o == null || getClass() != o.getClass()) return false;
7791
MonitoredArtifact artifact = (MonitoredArtifact) o;
78-
return Objects.equals(path, artifact.path) && Objects.equals(testResult, artifact.testResult) && Objects.equals(ignores, artifact.ignores);
92+
return Objects.equals(path, artifact.path) && Objects.equals(testResult, artifact.testResult) && Objects.equals(ignores, artifact.ignores) && Objects.equals(lastModifiedDate, artifact.lastModifiedDate);
7993
}
8094

8195
@Override
8296
public int hashCode() {
83-
return Objects.hash(path, testResult, ignores);
97+
return Objects.hash(path, testResult, ignores, lastModifiedDate);
8498
}
8599

86100
@Override
@@ -89,6 +103,7 @@ public String toString() {
89103
"path='" + path + '\'' +
90104
", testResult=" + testResult +
91105
", ignores=" + ignores +
106+
", lastModifiedDate=" + lastModifiedDate +
92107
'}';
93108
}
94109
}

core/src/main/java/io/snyk/plugins/artifactory/model/ValidationSettings.java

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,36 @@
55

66
import java.util.Optional;
77

8+
import static io.snyk.plugins.artifactory.configuration.PluginConfiguration.SCANNER_LAST_MODIFIED_DELAY_DAYS;
89
import static io.snyk.plugins.artifactory.configuration.PluginConfiguration.SCANNER_LICENSE_THRESHOLD;
910
import static io.snyk.plugins.artifactory.configuration.PluginConfiguration.SCANNER_VULNERABILITY_THRESHOLD;
1011

1112
public class ValidationSettings {
1213

1314
private final Optional<Severity> vulnSeverityThreshold;
14-
1515
private final Optional<Severity> licenseSeverityThreshold;
16+
private final Optional<Integer> lastModifiedDelayDays;
1617

1718
public ValidationSettings() {
18-
this(Optional.of(Severity.HIGH), Optional.of(Severity.HIGH));
19+
this(Optional.of(Severity.HIGH), Optional.of(Severity.HIGH), Optional.of(0));
1920
}
2021

21-
private ValidationSettings(Optional<Severity> vulnSeverityThreshold, Optional<Severity> licenseSeverityThreshold) {
22+
private ValidationSettings(Optional<Severity> vulnSeverityThreshold, Optional<Severity> licenseSeverityThreshold, Optional<Integer> lastModifiedDelayDays) {
2223
this.vulnSeverityThreshold = vulnSeverityThreshold;
2324
this.licenseSeverityThreshold = licenseSeverityThreshold;
25+
this.lastModifiedDelayDays = lastModifiedDelayDays;
2426
}
2527

2628
public ValidationSettings withVulnSeverityThreshold(Optional<Severity> threshold) {
27-
return new ValidationSettings(threshold, licenseSeverityThreshold);
29+
return new ValidationSettings(threshold, licenseSeverityThreshold, lastModifiedDelayDays);
2830
}
2931

3032
public ValidationSettings withLicenseSeverityThreshold(Optional<Severity> threshold) {
31-
return new ValidationSettings(vulnSeverityThreshold, threshold);
33+
return new ValidationSettings(vulnSeverityThreshold, threshold, lastModifiedDelayDays);
34+
}
35+
36+
public ValidationSettings withLastModifiedDelayDays(Optional<Integer> days) {
37+
return new ValidationSettings(vulnSeverityThreshold, licenseSeverityThreshold, days);
3238
}
3339

3440
public Optional<Severity> getVulnSeverityThreshold() {
@@ -39,18 +45,37 @@ public Optional<Severity> getLicenseSeverityThreshold() {
3945
return licenseSeverityThreshold;
4046
}
4147

48+
public Optional<Integer> getLastModifiedDelayDays() {
49+
return lastModifiedDelayDays;
50+
}
51+
4252
public static ValidationSettings from(ConfigurationModule config) {
4353
return from(
4454
config.getPropertyOrDefault(SCANNER_VULNERABILITY_THRESHOLD),
45-
config.getPropertyOrDefault(SCANNER_LICENSE_THRESHOLD)
55+
config.getPropertyOrDefault(SCANNER_LICENSE_THRESHOLD),
56+
config.getPropertyOrDefault(SCANNER_LAST_MODIFIED_DELAY_DAYS)
4657
);
4758
}
4859

4960
public static ValidationSettings from(String vulnThreshold, String licenseThreshold) {
61+
return from(vulnThreshold, licenseThreshold, "0");
62+
}
63+
64+
public static ValidationSettings from(String vulnThreshold, String licenseThreshold, String lastModifiedDelayDaysStr) {
5065
return new ValidationSettings(
5166
parseSeverity(vulnThreshold),
52-
parseSeverity(licenseThreshold)
67+
parseSeverity(licenseThreshold),
68+
parseLastModifiedDelayDays(lastModifiedDelayDaysStr)
5369
);
70+
}
71+
72+
private static Optional<Integer> parseLastModifiedDelayDays(String lastModifiedDelayDaysStr) {
73+
try {
74+
Integer lastModifiedDelayDays = Integer.parseInt(lastModifiedDelayDaysStr);
75+
return Optional.of(lastModifiedDelayDays);
76+
} catch(NumberFormatException e) {
77+
throw new IllegalArgumentException("Invalid value for last modified delay days: " + lastModifiedDelayDaysStr);
78+
}
5479
}
5580

5681
private static Optional<Severity> parseSeverity(String severityStr) {

core/src/main/java/io/snyk/plugins/artifactory/scanner/PackageValidator.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.slf4j.Logger;
99
import org.slf4j.LoggerFactory;
1010

11+
import java.time.Instant;
12+
import java.time.temporal.ChronoUnit;
1113
import java.util.Optional;
1214

1315
import static java.lang.String.format;
@@ -23,10 +25,40 @@ public PackageValidator(ValidationSettings settings) {
2325
}
2426

2527
public void validate(MonitoredArtifact artifact) {
28+
validateLastModifiedDelay(artifact);
2629
validateVulnerabilityIssues(artifact);
2730
validateLicenseIssues(artifact);
2831
}
2932

33+
private void validateLastModifiedDelay(MonitoredArtifact artifact) {
34+
Integer delayDays = settings.getLastModifiedDelayDays().get();
35+
if (delayDays == null || delayDays <= 0) {
36+
LOG.debug("Created delay is disabled ({} days)", delayDays);
37+
return;
38+
}
39+
40+
Optional<Instant> lastModifiedDate = artifact.getLastModifiedDate();
41+
if (lastModifiedDate.isEmpty()) {
42+
LOG.debug("Created date not available for {}, skipping created delay check", artifact.getPath());
43+
return;
44+
}
45+
46+
Instant now = Instant.now();
47+
long daysSinceLastModified = ChronoUnit.DAYS.between(lastModifiedDate.get(), now);
48+
49+
if (daysSinceLastModified < delayDays) {
50+
LOG.debug("Package created {} days ago, which is less than the delay of {} days: {}",
51+
daysSinceLastModified, delayDays, artifact.getPath());
52+
throw new CancelException(format(
53+
"Artifact was created %d days ago, which is less than the configured delay of %d days: %s",
54+
daysSinceLastModified, delayDays, artifact.getPath()
55+
), 403);
56+
}
57+
58+
LOG.debug("Package created {} days ago, which exceeds the delay of {} days: {}",
59+
daysSinceLastModified, delayDays, artifact.getPath());
60+
}
61+
3062
private void validateVulnerabilityIssues(MonitoredArtifact artifact) {
3163
validateIssues(
3264
artifact.getTestResult().getVulnSummary(),

core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerModule.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
import io.snyk.plugins.artifactory.model.TestResult;
1212
import io.snyk.plugins.artifactory.model.ValidationSettings;
1313
import org.artifactory.fs.FileLayoutInfo;
14+
import org.artifactory.fs.ItemInfo;
1415
import org.artifactory.repo.RepoPath;
1516
import org.artifactory.repo.Repositories;
17+
import org.artifactory.repo.RepositoryConfiguration;
1618
import org.jetbrains.annotations.NotNull;
1719
import org.slf4j.Logger;
1820
import org.slf4j.LoggerFactory;
1921

2022
import javax.annotation.Nonnull;
2123
import java.time.Duration;
24+
import java.time.Instant;
2225
import java.util.Optional;
2326

2427
import static java.util.Objects.requireNonNull;
@@ -94,13 +97,46 @@ private void filter(MonitoredArtifact artifact) {
9497

9598
private @NotNull MonitoredArtifact toMonitoredArtifact(TestResult testResult, @NotNull RepoPath repoPath) {
9699
Ignores ignores = Ignores.read(new RepositoryArtifactProperties(repoPath, repositories));
97-
return new MonitoredArtifact(repoPath.toString(), testResult, ignores);
100+
Instant lastModifiedDate = getLastModifiedDate(repoPath);
101+
102+
// Only apply lastModifiedDate to packages from remote repositories.
103+
if(lastModifiedDateRemoteOnly() && !isRemoteRepository(repoPath)) {
104+
lastModifiedDate = null;
105+
}
106+
return new MonitoredArtifact(repoPath.toString(), testResult, ignores, lastModifiedDate);
107+
}
108+
109+
private Instant getLastModifiedDate(RepoPath repoPath) {
110+
try {
111+
ItemInfo itemInfo = repositories.getItemInfo(repoPath);
112+
if (itemInfo != null) {
113+
Instant lastModified = Instant.ofEpochMilli(itemInfo.getLastModified());
114+
return lastModified;
115+
}
116+
} catch (Exception e) {
117+
LOG.debug("Could not retrieve last modified date for {}: {}", repoPath, e);
118+
}
119+
return null;
120+
}
121+
122+
private boolean isRemoteRepository(RepoPath repoPath) {
123+
String repoKey = repoPath.getRepoKey();
124+
RepositoryConfiguration repoConfig = repositories.getRepositoryConfiguration(repoKey);
125+
String repoType = repoConfig.getType();
126+
127+
LOG.debug("Found repository type: {}", repoType);
128+
129+
return repoType.toLowerCase().equals("remote");
98130
}
99131

100132
private boolean shouldTestContinuously() {
101133
return configurationModule.getPropertyOrDefault(PluginConfiguration.TEST_CONTINUOUSLY).equals("true");
102134
}
103135

136+
private boolean lastModifiedDateRemoteOnly() {
137+
return configurationModule.getPropertyOrDefault(PluginConfiguration.SCANNER_LAST_MODIFIED_CHECK_ONLY_REMOTE).equals("true");
138+
}
139+
104140
private Duration durationHoursProperty(PluginConfiguration property, ConfigurationModule configurationModule) {
105141
return Duration.ofHours(Integer.parseInt(configurationModule.getPropertyOrDefault(property)));
106142
}

core/src/test/java/io/snyk/plugins/artifactory/scanner/PackageValidatorTest.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.junit.jupiter.api.Test;
77

88
import java.net.URI;
9+
import java.time.Instant;
910
import java.util.Optional;
1011
import java.util.stream.Stream;
1112

@@ -141,4 +142,29 @@ void validate_includesSnykDetailsUrlInCancelException() {
141142
.isExactlyInstanceOf(CancelException.class)
142143
.hasMessageContaining("https://snyk.io/package/details");
143144
}
145+
146+
@Test
147+
void validate_includesLastModifiedDateDelay() {
148+
Integer lastModifiedDelayDays = 14;
149+
ValidationSettings settings = new ValidationSettings()
150+
.withLastModifiedDelayDays(Optional.of(lastModifiedDelayDays))
151+
.withVulnSeverityThreshold(Optional.empty())
152+
.withLicenseSeverityThreshold(Optional.empty());
153+
154+
PackageValidator validator = new PackageValidator(settings);
155+
156+
MonitoredArtifact artifact = new MonitoredArtifact("",
157+
new TestResult(
158+
IssueSummary.from(Stream.of(Severity.LOW)),
159+
IssueSummary.from(Stream.empty()),
160+
URI.create("https://snyk.io/package/details")
161+
),
162+
new Ignores(),
163+
Instant.now()
164+
);
165+
166+
assertThatThrownBy(() -> validator.validate(artifact))
167+
.isExactlyInstanceOf(CancelException.class)
168+
.hasMessageContaining("Artifact was created 0 days ago, which is less than the configured delay of 14 days");
169+
}
144170
}

0 commit comments

Comments
 (0)