2 * ============LICENSE_START=======================================================
3 * feature-state-management
4 * ================================================================================
5 * Copyright (C) 2017-2019 AT&T Intellectual Property. All rights reserved.
6 * ================================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * ============LICENSE_END=========================================================
21 package org.onap.policy.drools.statemanagement;
24 import java.io.FileInputStream;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.nio.file.FileVisitResult;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.nio.file.SimpleFileVisitor;
31 import java.nio.file.attribute.BasicFileAttributes;
32 import java.util.LinkedList;
33 import java.util.List;
34 import java.util.Properties;
36 import java.util.TreeSet;
37 import java.util.concurrent.TimeUnit;
38 import java.util.regex.Matcher;
39 import java.util.regex.Pattern;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
44 * This class audits the Maven repository.
46 public class RepositoryAudit extends DroolsPdpIntegrityMonitor.AuditBase {
47 // timeout in 60 seconds
48 private static final long DEFAULT_TIMEOUT = 60;
50 // get an instance of logger
51 private static final Logger logger = LoggerFactory.getLogger(RepositoryAudit.class);
53 // single global instance of this audit object
54 private static RepositoryAudit instance = new RepositoryAudit();
56 // Regex pattern used to find additional repos in the form "repository(number).id.url"
57 private static final Pattern repoPattern = Pattern.compile("(repository([1-9][0-9]*))[.]audit[.]id");
60 * Constructor - set the name to 'Repository'.
62 private RepositoryAudit() {
67 * Get the integrity monitor instance.
69 * @return the single 'RepositoryAudit' instance
71 public static DroolsPdpIntegrityMonitor.AuditBase getInstance() {
76 * First, get the names of each property from StateManagementProperties. For each property name, check if it is of
77 * the form "repository(number).audit.id" If so, we extract the number and determine if there exists another
78 * property in the form "repository(number).audit.url" with the same "number". Only the
79 * 'repository(number).audit.id' and 'repository(number).audit.url" properties need to be specified. If both 'id'
80 * and 'url' properties are found, we add it to our set. InvokeData.getProperty(String, boolean) will determine the
81 * other 4 properties: '*.username', '*.password', '*.is.active', and '*.ignore.errors', or use default values.
83 * @return set of Integers representing a repository to support
85 private static TreeSet<Integer> countAdditionalNexusRepos() {
86 TreeSet<Integer> returnIndices = new TreeSet<>();
87 Properties properties = StateManagementProperties.getProperties();
88 Set<String> propertyNames = properties.stringPropertyNames();
90 for (String currName : propertyNames) {
91 Matcher matcher = repoPattern.matcher(currName);
93 if (matcher.matches()) {
94 int currRepoNum = Integer.parseInt(matcher.group(2));
95 if (propertyNames.contains(matcher.group(1) + ".audit.url")) {
96 returnIndices.add(currRepoNum);
100 return returnIndices;
106 * @param properties properties to be passed to the audit
109 public void invoke(Properties properties) throws IOException, InterruptedException {
110 logger.debug("Running 'RepositoryAudit.invoke'");
112 InvokeData data = new InvokeData();
114 logger.debug("RepositoryAudit.invoke: repoAuditIsActive = {}" + ", repoAuditIgnoreErrors = {}",
115 data.repoAuditIsActive, data.repoAuditIgnoreErrors);
119 if (!data.isActive) {
120 logger.info("RepositoryAudit.invoke: exiting because isActive = {}", data.isActive);
124 // Run audit for first nexus repository
125 logger.debug("Running read-only audit on first nexus repository: repository");
128 // set of indices for supported nexus repos (ex: repository2 -> 2)
129 // TreeSet is used to maintain order so repos can be audited in numerical order
130 TreeSet<Integer> repoIndices = countAdditionalNexusRepos();
131 logger.debug("Additional nexus repositories: {}", repoIndices);
133 // Run audit for remaining 'numNexusRepos' repositories
134 for (int index : repoIndices) {
135 logger.debug("Running read-only audit on nexus repository = repository{}", index);
137 data = new InvokeData(index);
146 private void runAudit(InvokeData data) throws IOException, InterruptedException {
147 data.initIgnoreErrors();
151 * 1) create temporary directory
153 data.dir = Files.createTempDirectory("auditRepo");
154 logger.info("RepositoryAudit: temporary directory = {}", data.dir);
156 // nested 'pom.xml' file and 'repo' directory
157 final Path pom = data.dir.resolve("pom.xml");
158 final Path repo = data.dir.resolve("repo");
161 * 2) Create test file, and upload to repository (only if repository information is specified)
164 data.uploadTestFile();
168 * 3) create 'pom.xml' file in temporary directory
170 data.createPomFile(repo, pom);
173 * 4) Invoke external 'mvn' process to do the downloads
176 // output file = ${dir}/out (this supports step '4a')
177 File output = data.dir.resolve("out").toFile();
179 // invoke process, and wait for response
180 int rval = data.runMaven(output);
183 * 4a) Check attempted and successful downloads from output file Note: at present, this step just generates log
184 * messages, but doesn't do any verification.
186 if (rval == 0 && output != null) {
187 generateDownloadLogs(output);
191 * 5) Check the contents of the directory to make sure the downloads were successful
193 data.verifyDownloads(repo);
196 * 6) Use 'curl' to delete the uploaded test file (only if repository information is specified)
199 data.deleteUploadedTestFile();
203 * 7) Remove the temporary directory
205 Files.walkFileTree(data.dir, new RecursivelyDeleteDirectory());
210 * Set the response string to the specified value. Overrides 'setResponse(String value)' from
211 * DroolsPdpIntegrityMonitor This method prevents setting a response string that indicates whether the caller should
212 * receive an error list from the audit. By NOT setting the response string to a value, this indicates that there
215 * @param value the new value of the response string (null = no errors)
218 public void setResponse(String value) {
219 // Do nothing, prevent the caller from receiving a list of errors.
222 private class InvokeData {
223 private boolean isActive = true;
225 // ignore errors by default
226 private boolean ignoreErrors = true;
228 private final String repoAuditIsActive;
229 private final String repoAuditIgnoreErrors;
231 private final String repositoryId;
232 private final String repositoryUrl;
233 private final String repositoryUsername;
234 private final String repositoryPassword;
235 private final boolean upload;
237 // used to incrementally construct response as problems occur
238 // (empty = no problems)
239 private final StringBuilder response = new StringBuilder();
241 private long timeoutInSeconds = DEFAULT_TIMEOUT;
245 private String groupId = null;
246 private String artifactId = null;
247 private String version = null;
249 // artifacts to be downloaded
250 private final List<Artifact> artifacts = new LinkedList<>();
252 // 0 = base repository, 2-n = additional repositories
253 private final int index;
255 public InvokeData() {
259 public InvokeData(int index) {
261 repoAuditIsActive = getProperty("audit.is.active", true);
262 repoAuditIgnoreErrors = getProperty("audit.ignore.errors", true);
264 // Fetch repository information from 'IntegrityMonitorProperties'
265 repositoryId = getProperty("audit.id", false);
266 repositoryUrl = getProperty("audit.url", false);
267 repositoryUsername = getProperty("audit.username", true);
268 repositoryPassword = getProperty("audit.password", true);
270 logger.debug("Nexus Repository Information retrieved from 'IntegrityMonitorProperties':");
271 logger.debug("repositoryId: " + repositoryId);
272 logger.debug("repositoryUrl: " + repositoryUrl);
274 // Setting upload to be false so that files can no longer be created/deleted
278 private String getProperty(String property, boolean useDefault) {
279 String fullProperty = (index == 0 ? "repository." + property : "repository" + index + "." + property);
280 String rval = StateManagementProperties.getProperty(fullProperty);
281 if (rval == null && index != 0 && useDefault) {
282 rval = StateManagementProperties.getProperty("repository." + property);
287 public void initIsActive() {
288 if (repoAuditIsActive != null) {
290 isActive = Boolean.parseBoolean(repoAuditIsActive.trim());
291 } catch (NumberFormatException e) {
292 logger.warn("RepositoryAudit.invoke: Ignoring invalid property: repository.audit.is.active = {}",
296 if (repositoryId == null || repositoryUrl == null) {
301 public void initIgnoreErrors() {
302 if (repoAuditIgnoreErrors != null) {
304 ignoreErrors = Boolean.parseBoolean(repoAuditIgnoreErrors.trim());
305 } catch (NumberFormatException e) {
308 "RepositoryAudit.invoke: Ignoring invalid property: repository.audit.ignore.errors = {}",
309 repoAuditIgnoreErrors);
316 public void initTimeout() {
317 String timeoutString = getProperty("audit.timeout", true);
318 if (timeoutString != null && !timeoutString.isEmpty()) {
320 timeoutInSeconds = Long.valueOf(timeoutString);
321 } catch (NumberFormatException e) {
322 logger.error("RepositoryAudit: Invalid 'repository.audit.timeout' value: '{}'", timeoutString, e);
324 response.append("Invalid 'repository.audit.timeout' value: '").append(timeoutString)
326 setResponse(response.toString());
332 private void uploadTestFile() throws IOException, InterruptedException {
333 groupId = "org.onap.policy.audit";
334 artifactId = "repository-audit";
335 version = "0." + System.currentTimeMillis();
337 if (repositoryUrl.toLowerCase().contains("snapshot")) {
338 // use SNAPSHOT version
339 version += "-SNAPSHOT";
342 // create text file to write
343 try (FileOutputStream fos = new FileOutputStream(dir.resolve("repository-audit.txt").toFile())) {
344 fos.write(version.getBytes());
347 // try to install file in repository
348 if (runProcess(timeoutInSeconds, dir.toFile(), null, "mvn", "deploy:deploy-file",
349 "-DrepositoryId=" + repositoryId, "-Durl=" + repositoryUrl, "-Dfile=repository-audit.txt",
350 "-DgroupId=" + groupId, "-DartifactId=" + artifactId, "-Dversion=" + version, "-Dpackaging=txt",
351 "-DgeneratePom=false") != 0) {
352 logger.error("RepositoryAudit: 'mvn deploy:deploy-file' failed");
354 response.append("'mvn deploy:deploy-file' failed\n");
355 setResponse(response.toString());
358 logger.info("RepositoryAudit: 'mvn deploy:deploy-file succeeded");
360 // we also want to include this new artifact in the download
361 // test (steps 3 and 4)
362 artifacts.add(new Artifact(groupId, artifactId, version, "txt"));
366 private void createPomFile(final Path repo, final Path pom) throws IOException {
368 artifacts.add(new Artifact("org.apache.maven/maven-embedder/3.2.2"));
370 StringBuilder sb = new StringBuilder();
372 "<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
373 + " xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">\n"
374 + "\n" + " <modelVersion>4.0.0</modelVersion>\n" + " <groupId>empty</groupId>\n"
375 + " <artifactId>empty</artifactId>\n" + " <version>1.0-SNAPSHOT</version>\n"
376 + " <packaging>pom</packaging>\n" + "\n" + " <build>\n" + " <plugins>\n"
377 + " <plugin>\n" + " <groupId>org.apache.maven.plugins</groupId>\n"
378 + " <artifactId>maven-dependency-plugin</artifactId>\n"
379 + " <version>2.10</version>\n" + " <executions>\n"
380 + " <execution>\n" + " <id>copy</id>\n" + " <goals>\n"
381 + " <goal>copy</goal>\n" + " </goals>\n"
382 + " <configuration>\n" + " <localRepositoryDirectory>")
383 .append(repo).append("</localRepositoryDirectory>\n").append(" <artifactItems>\n");
385 for (Artifact artifact : artifacts) {
386 // each artifact results in an 'artifactItem' element
387 sb.append(" <artifactItem>\n" + " <groupId>").append(artifact.groupId)
388 .append("</groupId>\n" + " <artifactId>").append(artifact.artifactId)
389 .append("</artifactId>\n" + " <version>").append(artifact.version)
390 .append("</version>\n" + " <type>").append(artifact.type)
391 .append("</type>\n" + " </artifactItem>\n");
393 sb.append(" </artifactItems>\n" + " </configuration>\n"
394 + " </execution>\n" + " </executions>\n" + " </plugin>\n"
395 + " </plugins>\n" + " </build>\n" + "</project>\n");
397 try (FileOutputStream fos = new FileOutputStream(pom.toFile())) {
398 fos.write(sb.toString().getBytes());
402 private int runMaven(File output) throws IOException, InterruptedException {
403 int rval = runProcess(timeoutInSeconds, dir.toFile(), output, "mvn", "compile");
404 logger.info("RepositoryAudit: 'mvn' return value = {}", rval);
406 logger.error("RepositoryAudit: 'mvn compile' invocation failed");
408 response.append("'mvn compile' invocation failed\n");
409 setResponse(response.toString());
415 private void verifyDownloads(final Path repo) {
416 for (Artifact artifact : artifacts) {
417 if (repo.resolve(artifact.groupId.replace('.', '/')).resolve(artifact.artifactId)
418 .resolve(artifact.version)
419 .resolve(artifact.artifactId + "-" + artifact.version + "." + artifact.type).toFile()
421 // artifact exists, as expected
422 logger.info("RepositoryAudit: {} : exists", artifact.toString());
424 // Audit ERROR: artifact download failed for some reason
425 logger.error("RepositoryAudit: {}: does not exist", artifact.toString());
427 response.append("Failed to download artifact: ").append(artifact).append('\n');
428 setResponse(response.toString());
434 private void deleteUploadedTestFile() throws IOException, InterruptedException {
435 if (runProcess(timeoutInSeconds, dir.toFile(), null, "curl", "--request", "DELETE", "--user",
436 repositoryUsername + ":" + repositoryPassword,
437 repositoryUrl + "/" + groupId.replace('.', '/') + "/" + artifactId + "/" + version) != 0) {
438 logger.error("RepositoryAudit: delete of uploaded artifact failed");
440 response.append("delete of uploaded artifact failed\n");
441 setResponse(response.toString());
444 logger.info("RepositoryAudit: delete of uploaded artifact succeeded");
445 artifacts.add(new Artifact(groupId, artifactId, version, "txt"));
450 private void generateDownloadLogs(File output) throws IOException {
451 // place output in 'fileContents' (replacing the Return characters
453 byte[] outputData = new byte[(int) output.length()];
455 try (FileInputStream fis = new FileInputStream(output)) {
457 // Ideally this should be in a loop or even better use
458 // Java 8 nio functionality.
460 int bytesRead = fis.read(outputData);
461 logger.info("fileContents read {} bytes", bytesRead);
462 fileContents = new String(outputData).replace('\r', '\n');
465 // generate log messages from 'Downloading' and 'Downloaded'
466 // messages within the 'mvn' output
468 while ((index = fileContents.indexOf("\nDown", index)) > 0) {
470 if (fileContents.regionMatches(index, "loading: ", 0, 9)) {
472 int endIndex = fileContents.indexOf('\n', index);
473 logger.info("RepositoryAudit: Attempted download: '{}'", fileContents.substring(index, endIndex));
475 } else if (fileContents.regionMatches(index, "loaded: ", 0, 8)) {
477 int endIndex = fileContents.indexOf(' ', index);
478 logger.info("RepositoryAudit: Successful download: '{}'", fileContents.substring(index, endIndex));
485 * Run a process, and wait for the response.
487 * @param timeoutInSeconds the number of seconds to wait for the process to terminate
488 * @param directory the execution directory of the process (null = current directory)
489 * @param stdout the file to contain the standard output (null = discard standard output)
490 * @param command command and arguments
491 * @return the return value of the process
492 * @throws IOException InterruptedException
494 static int runProcess(long timeoutInSeconds, File directory, File stdout, String... command)
495 throws IOException, InterruptedException {
496 ProcessBuilder pb = new ProcessBuilder(command);
497 if (directory != null) {
498 pb.directory(directory);
500 if (stdout != null) {
501 pb.redirectOutput(stdout);
504 Process process = pb.start();
505 if (process.waitFor(timeoutInSeconds, TimeUnit.SECONDS)) {
506 // process terminated before the timeout
507 return process.exitValue();
510 // process timed out -- kill it, and return -1
511 process.destroyForcibly();
516 * This class is used to recursively delete a directory and all of its contents.
518 private final class RecursivelyDeleteDirectory extends SimpleFileVisitor<Path> {
520 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
521 file.toFile().delete();
522 return FileVisitResult.CONTINUE;
526 public FileVisitResult postVisitDirectory(Path file, IOException ex) throws IOException {
528 file.toFile().delete();
529 return FileVisitResult.CONTINUE;
536 /* ============================================================ */
539 * An instance of this class exists for each artifact that we are trying to download.
541 static class Artifact {
548 * Constructor - populate the 'Artifact' instance.
550 * @param groupId groupId of artifact
551 * @param artifactId artifactId of artifact
552 * @param version version of artifact
553 * @param type type of the artifact (e.g. "jar")
555 Artifact(String groupId, String artifactId, String version, String type) {
556 this.groupId = groupId;
557 this.artifactId = artifactId;
558 this.version = version;
563 * Constructor - populate an 'Artifact' instance.
565 * @param artifact a string of the form: {@code"<groupId>/<artifactId>/<version>[/<type>]"}
566 * @throws IllegalArgumentException if 'artifact' has the incorrect format
568 Artifact(String artifact) {
569 String[] segments = artifact.split("/");
570 if (segments.length != 4 && segments.length != 3) {
571 throw new IllegalArgumentException("groupId/artifactId/version/type");
573 groupId = segments[0];
574 artifactId = segments[1];
575 version = segments[2];
576 type = segments.length == 4 ? segments[3] : "jar";
580 * Returns string representation.
582 * @return the artifact id in the form: {@code"<groupId>/<artifactId>/<version>/<type>"}
585 public String toString() {
586 return groupId + "/" + artifactId + "/" + version + "/" + type;