blob: 5bff418e6cea4ee038b64fbb81d2cfc0d45df18a [file] [log] [blame]
package checks
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strings"
"github.com/Masterminds/semver"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/npm-audit-mirror/go/config"
"go.skia.org/infra/npm-audit-mirror/go/types"
)
var (
// Examples of what the package tarball requests can look like:
// * /import-fresh/-/import-fresh-3.3.0.tgz
// * /@types/web/-/web-0.0.58.tgz
// * /@google-web-components/google-chart/-/google-chart-4.0.2.tgz
// * /gensync/-/gensync-1.0.0-beta.2.tgz
packageTarballRequestRegex = regexp.MustCompile("/((@.+/)?(.+))/-/((.+).tgz)")
)
// NpmChecksManager implements types.ChecksManager.
type NpmChecksManager struct {
trustedScopes []string
allowedPackages []*config.PackagesAllowList
httpClient *http.Client
projectMirror types.ProjectMirror
checks []types.Check
}
// NewNpmChecksManager returns an instance of NpmChecksManager.
func NewNpmChecksManager(trustedScopes []string, allowedPackages []*config.PackagesAllowList, httpClient *http.Client, projectMirror types.ProjectMirror) types.ChecksManager {
// The checks manager will perform the following checks for packages.
checks := []types.Check{NewPublishAgeCheck(), NewLicenceCheck()}
return &NpmChecksManager{
trustedScopes: trustedScopes,
allowedPackages: allowedPackages,
httpClient: httpClient,
projectMirror: projectMirror,
checks: checks,
}
}
// getPackageDetails parses the provided requestURL and returns a populated
// PackageDetails obj.
func getPackageDetails(packageRequestURL string) *types.PackageDetails {
// Replace all "%2f" with "/" in the request URL.
packageRequestURL = strings.ReplaceAll(packageRequestURL, "%2f", "/")
match := packageTarballRequestRegex.FindStringSubmatch(packageRequestURL)
return &types.PackageDetails{
NameWithScope: match[1],
ScopeName: match[2],
TarballName: match[4],
Version: strings.TrimPrefix(match[5], match[3]+"-"),
}
}
// PerformChecks implements the types.ChecksManager interface.
func (ncm *NpmChecksManager) PerformChecks(packageRequestURL string) (bool, string, error) {
// We only perform checks on requests for package tarballs. Because this is when
// the package is downloaded to the developer/CI machine.
if !strings.HasSuffix(packageRequestURL, ".tgz") {
return true, "", nil
}
// Get package details.
packageDetails := getPackageDetails(packageRequestURL)
// Check to see if the packageTarball is in this project's installed packages.
if ncm.projectMirror.IsPackageTarballDownloaded(packageDetails.TarballName) {
// No need to perform checks on already downloaded packages.
sklog.Infof("Package %s is already downloaded to project %s mirror. Skipping security checks.", packageDetails.TarballName, ncm.projectMirror.GetProjectName())
return true, "", nil
}
// Check for trusted scopes.
for _, trustedScope := range ncm.trustedScopes {
if packageDetails.ScopeName == trustedScope {
sklog.Infof("The package %s has the trusted scope %s. Skipping security checks.", packageDetails.NameWithScope, trustedScope)
return true, "", nil
}
}
// Check for packages in the allowlist.
packageSemVer, err := semver.NewVersion(packageDetails.Version)
if err != nil {
sklog.Errorf("Could not parse semver in %s: %s", packageDetails.Version, err)
return false, "", err
}
for _, ap := range ncm.allowedPackages {
if ap.Name == packageDetails.NameWithScope {
allowedSemVerConstaint, err := semver.NewConstraint(ap.Version)
if err != nil {
sklog.Errorf("Could not parse version of package %s@%s in %s: %s", ap.Name, ap.Version, err)
return false, "", err
}
if allowedSemVerConstaint.Check(packageSemVer) {
sklog.Infof("The package %s with version %s matched the semantic versioning of allowed package %s@%s. Skipping security checks.", packageDetails.NameWithScope, packageDetails.Version, ap.Name, ap.Version)
return true, "", nil
}
}
}
// Call registry.npmjs.org to run checks on the package before allowing the
// mirror to download it.
viewNpmURL := fmt.Sprintf("https://registry.npmjs.org/%s", packageDetails.NameWithScope)
r, err := ncm.httpClient.Get(viewNpmURL)
if err != nil {
return false, "", skerr.Wrapf(err, "Error getting response from %s", viewNpmURL)
}
defer r.Body.Close()
var npmPackage types.NpmPackage
if err := json.NewDecoder(r.Body).Decode(&npmPackage); err != nil {
return false, "", skerr.Wrapf(err, "Failed to decode response from %s", viewNpmURL)
}
// Start the security checks.
for _, check := range ncm.checks {
result, resultReason, err := check.PerformCheck(packageDetails.NameWithScope, packageDetails.Version, &npmPackage)
if err != nil {
sklog.Errorf("Error performing check %s on package %s with version %s", check.Name(), packageDetails.NameWithScope, packageDetails.Version)
return false, "", err
}
if result {
sklog.Infof("The check %s succeeded for package %s with version %s", check.Name(), packageDetails.NameWithScope, packageDetails.Version)
} else {
sklog.Warningf("The check %s failed for package %s with version %s due to: %s", check.Name(), packageDetails.NameWithScope, packageDetails.Version, resultReason)
return false, resultReason, nil
}
}
// If we reach here then all security passes succeeded. Add the package to the mirror.
sklog.Infof("Add the package %s to the in-memory map of downloaded tarballs for project %s.", packageDetails.TarballName, ncm.projectMirror.GetProjectName())
ncm.projectMirror.AddToDownloadedPackageTarballs(packageDetails.TarballName)
return true, "", nil
}