-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce AddSecurityScanWorkflow Recipe #36
base: main
Are you sure you want to change the base?
Changes from 2 commits
901a886
37448cc
f7d9b97
c3e7789
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
/* | ||
* Copyright 2023 the original author or authors. | ||
* <p> | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.openrewrite.jenkins.github; | ||
|
||
import lombok.EqualsAndHashCode; | ||
import lombok.Value; | ||
import org.intellij.lang.annotations.Language; | ||
import org.openrewrite.*; | ||
import org.openrewrite.internal.lang.Nullable; | ||
import org.openrewrite.yaml.ChangePropertyValue; | ||
import org.openrewrite.yaml.JsonPathMatcher; | ||
import org.openrewrite.yaml.YamlIsoVisitor; | ||
import org.openrewrite.yaml.YamlParser; | ||
import org.openrewrite.yaml.tree.Yaml; | ||
|
||
import java.io.BufferedReader; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.InputStreamReader; | ||
import java.nio.file.Path; | ||
import java.nio.file.Paths; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.LinkedList; | ||
import java.util.List; | ||
import java.util.concurrent.atomic.AtomicBoolean; | ||
import java.util.stream.Collectors; | ||
|
||
import static java.nio.charset.StandardCharsets.UTF_8; | ||
import static java.util.Objects.requireNonNull; | ||
|
||
@Value | ||
@EqualsAndHashCode(callSuper = true) | ||
public class AddSecurityScanWorkflow extends ScanningRecipe<AtomicBoolean> { | ||
private static final String FILE_PATH = ".github/workflows/jenkins-security-scan.yml"; | ||
private static final String DEFAULT_WORKFLOW_PATH = "/org/openrewrite/jenkins/github/jenkins-security-scan.yml"; | ||
private static final JsonPathMatcher BRANCHES_KEY = new JsonPathMatcher("$.on.push.branches"); | ||
private static final String JAVA_VERSION_KEY = "jobs.security-scan.with.java-version"; | ||
private static final String BUILD_TOOL_KEY = "jobs.security-scan.with.java-cache"; | ||
|
||
@Option(displayName = "Branches", | ||
description = "Run workflow on push to these branches.", | ||
example = "main", | ||
required = false) | ||
@Nullable | ||
List<String> branches; | ||
|
||
@Option(displayName = "Java Version", | ||
description = "Version of Java to set for build.", | ||
example = "11", | ||
required = false) | ||
@Nullable | ||
Integer javaVersion; | ||
|
||
@Option(displayName = "Build Tool", | ||
description = "Set up dependency cache.", | ||
example = "maven", | ||
valid = {"maven", "gradle"}, | ||
required = false) | ||
@Nullable | ||
String buildTool; | ||
Comment on lines
+68
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the GitProvenance marker, we also have markers for the BuildTool and LstProvenance. Both of those contain a |
||
|
||
@Override | ||
public String getDisplayName() { | ||
return "Add Jenkins Security Scan workflow"; | ||
} | ||
|
||
@Override | ||
public String getDescription() { | ||
return "Adds the Jenkins Security Scan GitHub Actions workflow. " + | ||
"See [docs](https://www.jenkins.io/doc/developer/security/scan/) for details."; | ||
} | ||
|
||
@Override | ||
public AtomicBoolean getInitialValue(ExecutionContext ctx) { | ||
return new AtomicBoolean(); | ||
} | ||
|
||
@Override | ||
public TreeVisitor<?, ExecutionContext> getScanner(AtomicBoolean found) { | ||
Path path = Paths.get(FILE_PATH); | ||
return new TreeVisitor<Tree, ExecutionContext>() { | ||
@Override | ||
public Tree visit(@Nullable Tree tree, ExecutionContext executionContext, Cursor parent) { | ||
SourceFile sourceFile = (SourceFile) requireNonNull(tree); | ||
if (path.toString().equals(sourceFile.getSourcePath().toString())) { | ||
found.set(true); | ||
} | ||
return sourceFile; | ||
} | ||
}; | ||
} | ||
|
||
@Override | ||
public Collection<? extends SourceFile> generate(AtomicBoolean found, ExecutionContext ctx) { | ||
if (found.get()) { | ||
return Collections.emptyList(); | ||
} | ||
YamlParser parser = new YamlParser(); | ||
@Language("yml") | ||
timtebeek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
String workflow = defaultWorkflow(); | ||
return parser.parse(workflow) | ||
.map(brandNewFile -> (Yaml.Documents) brandNewFile.withSourcePath(Paths.get(FILE_PATH))) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
@Override | ||
public TreeVisitor<?, ExecutionContext> getVisitor(AtomicBoolean found) { | ||
return new YamlIsoVisitor<ExecutionContext>() { | ||
@Override | ||
public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext executionContext) { | ||
timtebeek marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (branches != null) { | ||
doAfterVisit(new ReplaceSequenceVisitor(BRANCHES_KEY, branches)); | ||
} | ||
if (javaVersion != null) { | ||
doAfterVisit(new ChangePropertyValue( | ||
JAVA_VERSION_KEY, | ||
String.valueOf(javaVersion), | ||
null, | ||
null, | ||
false | ||
).getVisitor()); | ||
} | ||
if (buildTool != null) { | ||
doAfterVisit(new ChangePropertyValue( | ||
BUILD_TOOL_KEY, | ||
buildTool, | ||
null, | ||
null, | ||
false | ||
).getVisitor()); | ||
} | ||
return super.visitDocuments(documents, executionContext); | ||
} | ||
}; | ||
} | ||
|
||
private static String defaultWorkflow() { | ||
try (InputStream is = AddSecurityScanWorkflow.class.getResourceAsStream(DEFAULT_WORKFLOW_PATH)) { | ||
requireNonNull(is); | ||
try (BufferedReader br = new BufferedReader(new InputStreamReader(is, UTF_8))) { | ||
List<String> wanted = new LinkedList<>(); | ||
List<String> lines = br.lines().collect(Collectors.toList()); | ||
boolean licenseHeaderDone = false; | ||
for (String line : lines) { | ||
if (licenseHeaderDone) { | ||
wanted.add(line); | ||
} else if (line.isEmpty()) { | ||
licenseHeaderDone = true; | ||
} | ||
} | ||
Comment on lines
+156
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense to configure the license plugin to the workflow file with something like this? license {
exclude "**/*.properties"
excludes(["**/*.txt", "**/*.conf"])
} Unless we go for downloading the file on the fly of course, in which case this wouldn't be necessary. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, thanks! Didn't know this was an option |
||
return String.join("\n", wanted); | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/* | ||
* Copyright 2023 the original author or authors. | ||
* <p> | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.openrewrite.jenkins.github; | ||
|
||
import org.openrewrite.Cursor; | ||
import org.openrewrite.ExecutionContext; | ||
import org.openrewrite.marker.Markers; | ||
import org.openrewrite.yaml.JsonPathMatcher; | ||
import org.openrewrite.yaml.YamlIsoVisitor; | ||
import org.openrewrite.yaml.tree.Yaml; | ||
|
||
import java.util.List; | ||
|
||
import static org.openrewrite.Tree.randomId; | ||
|
||
/** | ||
* Adapted from {@link org.openrewrite.yaml.AppendToSequenceVisitor} | ||
*/ | ||
class ReplaceSequenceVisitor extends YamlIsoVisitor<ExecutionContext> { | ||
Comment on lines
+30
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Glad to see you were able to make this work; I'm wondering if it would make sense to pull this up, or change either There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do think that would make sense. I actually checked both those recipes to see if they already did this. I think it might make sense for |
||
private final JsonPathMatcher matcher; | ||
private final List<String> values; | ||
|
||
ReplaceSequenceVisitor(JsonPathMatcher matcher, List<String> values) { | ||
this.matcher = matcher; | ||
this.values = values; | ||
} | ||
|
||
@Override | ||
public Yaml.Sequence visitSequence(Yaml.Sequence existingSeq, ExecutionContext executionContext) { | ||
Cursor parent = getCursor().getParent(); | ||
if (matcher.matches(parent) && !alreadyVisited(existingSeq, executionContext)) { | ||
setVisited(existingSeq, executionContext); | ||
Yaml.Sequence newSeq = replaceSequence(existingSeq, this.values); | ||
setVisited(newSeq, executionContext); | ||
return newSeq; | ||
} | ||
return super.visitSequence(existingSeq, executionContext); | ||
} | ||
|
||
private Yaml.Sequence replaceSequence(Yaml.Sequence existingSequence, List<String> values) { | ||
Yaml.Sequence newSequence = existingSequence.copyPaste(); | ||
List<Yaml.Sequence.Entry> entries = newSequence.getEntries(); | ||
boolean hasDash = false; | ||
Yaml.Scalar.Style style = Yaml.Scalar.Style.PLAIN; | ||
String entryPrefix = ""; | ||
String entryTrailingCommaPrefix = null; | ||
String itemPrefix = ""; | ||
if (!entries.isEmpty()) { | ||
final int lastEntryIndex = entries.size() - 1; | ||
Yaml.Sequence.Entry existingEntry = entries.get(lastEntryIndex); | ||
hasDash = existingEntry.isDash(); | ||
entryPrefix = existingEntry.getPrefix(); | ||
entryTrailingCommaPrefix = existingEntry.getTrailingCommaPrefix(); | ||
Yaml.Sequence.Block block = existingEntry.getBlock(); | ||
if (block instanceof Yaml.Sequence.Mapping) { | ||
Yaml.Sequence.Mapping mapping = (Yaml.Sequence.Mapping) block; | ||
List<Yaml.Mapping.Entry> mappingEntries = mapping.getEntries(); | ||
if (!mappingEntries.isEmpty()) { | ||
Yaml.Mapping.Entry entry = mappingEntries.get(0); | ||
itemPrefix = entry.getPrefix(); | ||
} | ||
} | ||
else if (block instanceof Yaml.Sequence.Scalar) { | ||
itemPrefix = block.getPrefix(); | ||
style = ((Yaml.Sequence.Scalar) block).getStyle(); | ||
} | ||
if (!existingEntry.isDash()) { | ||
entries.set(lastEntryIndex, existingEntry.withTrailingCommaPrefix("")); | ||
} | ||
} | ||
entries.clear(); | ||
for (String value : values) { | ||
Yaml.Scalar newItem = new Yaml.Scalar(randomId(), itemPrefix, Markers.EMPTY, style, null, value); | ||
Yaml.Sequence.Entry newEntry = new Yaml.Sequence.Entry(randomId(), entryPrefix, Markers.EMPTY, newItem, hasDash, entryTrailingCommaPrefix); | ||
entries.add(newEntry); | ||
} | ||
return newSequence; | ||
} | ||
|
||
private static void setVisited(Yaml.Sequence seq, ExecutionContext context) { | ||
context.putMessage(makeAlreadyVisitedKey(seq), Boolean.TRUE); | ||
} | ||
|
||
private static boolean alreadyVisited(Yaml.Sequence seq, ExecutionContext context) { | ||
return context.getMessage(makeAlreadyVisitedKey(seq), Boolean.FALSE); | ||
} | ||
|
||
private static String makeAlreadyVisitedKey(Yaml.Sequence seq) { | ||
return org.openrewrite.yaml.AppendToSequenceVisitor.class.getName() + ".alreadyVisited." + seq.getId().toString(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# | ||
# Copyright 2023 the original author or authors. | ||
# <p> | ||
# Licensed under the Apache License, Version 2.0 (the "License"); | ||
# you may not use this file except in compliance with the License. | ||
# You may obtain a copy of the License at | ||
# <p> | ||
# https://www.apache.org/licenses/LICENSE-2.0 | ||
# <p> | ||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, | ||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
# See the License for the specific language governing permissions and | ||
# limitations under the License. | ||
# | ||
|
||
# More information about the Jenkins security scan can be found at the developer docs: https://www.jenkins.io/redirect/jenkins-security-scan/ | ||
|
||
name: Jenkins Security Scan | ||
on: | ||
push: | ||
branches: | ||
- "master" | ||
- "main" | ||
pull_request: | ||
types: [ opened, synchronize, reopened ] | ||
workflow_dispatch: | ||
|
||
permissions: | ||
security-events: write | ||
contents: read | ||
actions: read | ||
|
||
jobs: | ||
security-scan: | ||
uses: jenkins-infra/jenkins-security-scan/.github/workflows/jenkins-security-scan.yaml@v2 | ||
with: | ||
java-cache: 'maven' # Optionally enable use of a build dependency cache. Specify 'maven' or 'gradle' as appropriate. | ||
java-version: 11 # What version of Java to set up for the build. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In what you might have come to expect by now: we also have a JavaVersion marker that you can use to determine the Java version. You might want to account for projects that use a different version for src/main versus src/test, but other than that it should be convenient not to have to pass this parameter I think.