Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -612,26 +613,77 @@ Set<String> getCustomNpmRegistries(File workingDirectory) {
* map if the configuration cannot be read
*/
Map<String, String> getConfiguredRegistries(File workingDirectory) {
List<String> command = new ArrayList<>(getNpmExecutable(false));
JsonNode config = getResolvedConfiguration(getNpmExecutable(false),
workingDirectory);
Map<String, String> registries = new HashMap<>();
for (String key : config.propertyNames()) {
if ((key.equals("registry") || key.endsWith(":registry"))
&& config.get(key).isString()) {
registries.put(key, config.get(key).asString());
}
}
return registries;
}

/**
* Reads the value the given npm or pnpm command resolves for a
* configuration key.
*
* @param toolCommand
* the npm or pnpm command to run
* @param key
* the configuration key to read
* @param workingDirectory
* the directory the configuration is resolved from, so that a
* project {@code .npmrc} is taken into account
* @return the configured value, or an empty optional if the key is not
* configured, is unknown to the tool, or the configuration cannot
* be read
*/
Optional<String> getConfiguredSetting(List<String> toolCommand, String key,
File workingDirectory) {
JsonNode value = getResolvedConfiguration(toolCommand, workingDirectory)
.get(key);
// npm lists every key it knows, using null for the ones that are not
// configured; pnpm lists only the configured ones
if (value == null || value.isNull()) {
return Optional.empty();
}
return Optional.of(value.asString());
}

/**
* Reads the configuration the given npm or pnpm command resolves for a
* directory by running {@code config ls --json}.
* <p>
* The configuration is read from the tool itself, so it accounts for every
* configuration source and precedence rule the tool applies (command line,
* environment variables, project/user/global/builtin {@code .npmrc} and,
* for pnpm, {@code pnpm-workspace.yaml}).
*
* @param toolCommand
* the npm or pnpm command to run
* @param workingDirectory
* the directory the configuration is resolved from, so that a
* project {@code .npmrc} is taken into account
* @return the resolved configuration, or an empty object if it cannot be
* read
*/
JsonNode getResolvedConfiguration(List<String> toolCommand,
File workingDirectory) {
List<String> command = new ArrayList<>(toolCommand);
command.add("config");
command.add("ls");
command.add("--json");
Map<String, String> registries = new HashMap<>();
try {
String output = FrontendUtils.executeCommand(command,
builder -> builder.directory(workingDirectory));
JsonNode config = JacksonUtils.readTree(output);
for (String key : config.propertyNames()) {
if ((key.equals("registry") || key.endsWith(":registry"))
&& config.get(key).isString()) {
registries.put(key, config.get(key).asString());
}
}
return JacksonUtils.readTree(output);
} catch (CommandExecutionException | RuntimeException e) {
getLogger().debug("Could not read the npm registry configuration; "
+ "assuming the default registry.", e);
getLogger().debug("Could not read the configuration using '{}'",
String.join(" ", command), e);
return JacksonUtils.createObjectNode();
}
return registries;
}

/**
Expand Down Expand Up @@ -677,9 +729,9 @@ public FrontendVersion getNpmVersion() throws UnknownVersionException {
* the {@code --min-release-age} install flag (see
* {@link #MIN_NPM_VERSION_FOR_RELEASE_AGE}). Used when building the
* {@code npm install} command for the minimum-package-age check (see
* {@link Options#withMinimumFrontendPackageAgeDays(int)}) to decide between
* {@code --min-release-age} and the {@code --before=<date>} fallback
* supported by older npm versions.
* {@link Options#withMinimumFrontendPackageAgeDays(Integer)}) to decide
* between {@code --min-release-age} and the {@code --before=<date>}
* fallback supported by older npm versions.
*
* @param npmCommand
* the npm command to invoke for {@code --version}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Optional;
import java.util.Set;

import org.jspecify.annotations.Nullable;
import org.slf4j.LoggerFactory;
import tools.jackson.databind.JsonNode;

Expand Down Expand Up @@ -175,11 +176,12 @@ public class Options implements Serializable {

/**
* Minimum age, in days, that an npm/pnpm/bun frontend package version must
* have before it is allowed to be installed. Defaults to {@code 1} day as a
* mitigation against malicious packages published to the registry; set to
* {@code 0} to disable.
* have before it is allowed to be installed, or {@code null} when nothing
* has been configured on the Vaadin side. In the latter case the value is
* resolved from the package manager configuration, falling back to
* {@link TaskRunNpmInstall#DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS}.
*/
private int minimumFrontendPackageAgeDays = 1;
private @Nullable Integer minimumFrontendPackageAgeDays;

private ApplicationConfiguration applicationConfiguration;

Expand Down Expand Up @@ -1179,21 +1181,34 @@ public Options withCommercialBanner(boolean enableCommercialBanner) {
* to avoid pulling in brand-new versions that may have been compromised by
* a supply-chain attack but not yet detected and removed from the registry.
* <p>
* For npm this is translated to a {@code --before=<date>} argument; for
* pnpm it becomes {@code --config.minimum-release-age=<minutes>} (requires
* pnpm &ge; 10.16.0); for bun it becomes
* {@code --minimum-release-age=<seconds>} (requires bun &ge; 1.3.0).
* For npm this is translated to a {@code --min-release-age=<days>}
* argument, or {@code --before=<date>} for npm older than 11.10.0; for pnpm
* it becomes {@code --config.minimum-release-age=<minutes>} (requires pnpm
* &ge; 10.16.0); for bun it becomes {@code --minimum-release-age=<seconds>}
* (requires bun &ge; 1.3.0). Since these are command line arguments, they
* take precedence over anything the package manager reads from its own
* configuration.
* <p>
* When set to {@code null}, no such argument is passed if the package
* manager already resolves a minimum release age from its own configuration
* ({@code .npmrc}, {@code pnpm-workspace.yaml} or {@code bunfig.toml}), so
* that a manually run {@code npm install} behaves the same way. Only if
* nothing is configured there,
* {@link TaskRunNpmInstall#DEFAULT_MINIMUM_FRONTEND_PACKAGE_AGE_DAYS} is
* used.
*
* @param minimumFrontendPackageAgeDays
* minimum allowed age in days, or {@code 0} to disable the check
* minimum allowed age in days, {@code 0} to disable the check,
* or {@code null} to use the package manager configuration
* @return this builder
* @throws IllegalArgumentException
* if {@code minimumFrontendPackageAgeDays} is negative
* @since 25.1.6
*/
public Options withMinimumFrontendPackageAgeDays(
int minimumFrontendPackageAgeDays) {
if (minimumFrontendPackageAgeDays < 0) {
@Nullable Integer minimumFrontendPackageAgeDays) {
if (minimumFrontendPackageAgeDays != null
&& minimumFrontendPackageAgeDays < 0) {
throw new IllegalArgumentException(
"minimumFrontendPackageAgeDays must be >= 0");
}
Expand All @@ -1204,12 +1219,15 @@ public Options withMinimumFrontendPackageAgeDays(
/**
* Gets the minimum age (in days) a frontend package version must have
* before npm, pnpm or bun is allowed to install it. {@code 0} means the
* check is disabled.
* check is disabled and {@code null} means that nothing has been configured
* on the Vaadin side, in which case the package manager configuration
* decides. See {@link #withMinimumFrontendPackageAgeDays(Integer)}.
*
* @return the minimum allowed age in days
* @return the minimum allowed age in days, or {@code null} if not
* configured
* @since 25.1.6
*/
public int getMinimumFrontendPackageAgeDays() {
public @Nullable Integer getMinimumFrontendPackageAgeDays() {
return minimumFrontendPackageAgeDays;
}

Expand Down
Loading
Loading