Skip to content
Merged
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
290 changes: 290 additions & 0 deletions malwaredb/verdict.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
package malwaredb

import (
"fmt"
"strings"
)

const (
SourceOpenSSF = "OpenSSF"
SourceBlockList = "MalwareBlockList"
SourceAllowList = "MalwareAllowList"
SourceTrustedModules = "TrustedModules"
SourceApprovedModules = "ApprovedModules"
SourceApprovedGlobal = "ApprovedGlobalModules"
SourceRemote = "remote"
SourceSkip = "skip"
SourceMissingDB = "missing_db"
SourceNone = ""

ReasonOpenSSF = "recognized as malware"
ReasonBlockList = "organization malware block"
ReasonRemote = "remote malware policy"
ReasonAllowList = "OpenSSF hit allowed"
ReasonBlockListAllow = "organization malware block allowed"
ReasonSkip = "malware checks skipped"
ReasonMissingDB = "malware definitions are missing or invalid"
ReasonClear = ""

MsgOpenSSF = "recognized as malware by OpenSSF"
MsgBlockList = "blocked by organization malware policy"
MsgAllowList = "OpenSSF block overruled by malware acceptance policy"
MsgTrustOSSF = "OpenSSF block overruled by module trust policy"
MsgTrustLocal = "Malware block overruled by module trust policy"
MsgApprovedOSSF = "OpenSSF block overruled by approved module policy"
MsgApprovedLocal = "Malware block overruled by approved module policy"
MsgApprovedGlobalOSSF = "OpenSSF block overruled by approved global module policy"
MsgApprovedGlobalLocal = "Malware block overruled by approved global module policy"
MsgSkip = "malware checks skipped"
MsgMissingDB = "malware definitions are missing or invalid"
MsgNone = "no malware detected"
)

// Module is a name plus optional version from status args or an install token.
type Module struct {
Raw string
Name string
Version string
}

// Row is one local malware verdict. Remote HTTPS is never consulted.
type Row struct {
Name string `json:"name"`
Version string `json:"version,omitempty"`
Blocked bool `json:"blocked"`
Status string `json:"status"`
Source string `json:"source,omitempty"`
Reason string `json:"-"`
Message string `json:"message"`
RemoteConfigured bool `json:"remote_configured,omitempty"`
Remote string `json:"remote,omitempty"`
}

// Input is the local data Check needs. Lists must already be HKLM policy values.
type Input struct {
Certified bool
Skip bool
AllowList []string
BlockList []string
RemoteConfigured bool
Trusted bool
Approved bool
ApprovedGlobal bool
TrustConflict string
DB *DB
DBErr error
}

// ParseModule splits left-pad, [email protected], and @scope/[email protected].
func ParseModule(raw string) Module {
raw = strings.TrimSpace(raw)
name, ver, hasVer := splitPattern(raw)
if !hasVer {
return Module{Raw: raw, Name: raw}
}
return Module{Raw: raw, Name: name, Version: ver}
}

func patternHit(rules []string, name, version string) bool {
for _, raw := range rules {
e := strings.TrimSpace(raw)
if e == "" {
continue
}
if len(e) >= 8 && strings.EqualFold(e[:8], "https://") {
continue
}
if MatchPattern(e, name, version) {
return true
}
}
return false
}

// Check one module against local inputs. Does not POST the org URL.
func Check(mod Module, in Input) Row {
row := Row{
Name: mod.Name,
Version: mod.Version,
RemoteConfigured: in.RemoteConfigured,
}
if in.RemoteConfigured {
row.Remote = "configured, not consulted"
}
if in.Skip {
row.Source = SourceSkip
row.Reason = ReasonSkip
return finalize(row)
}
if in.DB == nil || in.DBErr != nil {
row.Blocked = true
row.Source = SourceMissingDB
row.Reason = ReasonMissingDB
return finalize(row)
}

local := in.Certified && patternHit(in.BlockList, mod.Name, mod.Version)
ossf, err := in.DB.Lookup(mod.Name, mod.Version)
if err != nil {
row.Blocked = true
row.Source = SourceMissingDB
row.Reason = ReasonMissingDB
return finalize(row)
}
allow := ossf && patternHit(in.AllowList, mod.Name, mod.Version)

hit := HitNone
if local {
hit = HitLocal
} else if ossf {
hit = HitOSSF
}
edition := Community
if in.Certified {
edition = Certified
}
granted := in.Trusted || in.Approved || in.ApprovedGlobal
blocked := Block(VerdictInput{
Edition: edition,
Hit: hit,
Allow: allow,
Trusted: granted,
TrustConflict: in.TrustConflict,
})
row.Blocked = blocked
switch {
case local && blocked:
row.Source = SourceBlockList
row.Reason = ReasonBlockList
case ossf && allow && !blocked:
row.Source = SourceAllowList
row.Reason = ReasonAllowList
case !blocked && granted && (local || ossf):
row.Source = grantSource(in)
if local {
row.Reason = ReasonBlockListAllow
} else {
row.Reason = ReasonAllowList
}
case ossf && blocked:
row.Source = SourceOpenSSF
row.Reason = ReasonOpenSSF
}
return finalize(row)
}

func finalize(row Row) Row {
row.Status = StatusLabel(row.Blocked)
row.Message = StatusMessage(row)
return row
}

func StatusLabel(blocked bool) string {
if blocked {
return "blocked"
}
return "allowed"
}

func StatusMessage(row Row) string {
switch row.Source {
case SourceOpenSSF:
return MsgOpenSSF
case SourceBlockList:
return MsgBlockList
case SourceAllowList:
return MsgAllowList
case SourceTrustedModules:
if row.Reason == ReasonBlockListAllow {
return MsgTrustLocal
}
return MsgTrustOSSF
case SourceApprovedModules:
if row.Reason == ReasonBlockListAllow {
return MsgApprovedLocal
}
return MsgApprovedOSSF
case SourceApprovedGlobal:
if row.Reason == ReasonBlockListAllow {
return MsgApprovedGlobalLocal
}
return MsgApprovedGlobalOSSF
case SourceSkip:
return MsgSkip
case SourceMissingDB:
return MsgMissingDB
default:
return MsgNone
}
}

func grantSource(in Input) string {
if in.Trusted {
return SourceTrustedModules
}
if in.Approved {
return SourceApprovedModules
}
if in.ApprovedGlobal {
return SourceApprovedGlobal
}
return SourceNone
}

// FormatStatusLine is one TSV status row: module, status, message.
func FormatStatusLine(row Row) string {
spec := row.Name
if row.Version != "" {
spec += "@" + row.Version
}
return fmt.Sprintf("%s\t%s\t%s", spec, StatusLabel(row.Blocked), StatusMessage(row))
}

// FormatInstallLine is the shim/install NVM4602 sentence (reason already final).
func FormatInstallLine(spec, source, reason string) string {
body := installBody(source, reason)
return fmt.Sprintf("NVM blocked %s — %s. (NVM4602)", spec, body)
}

func installBody(source, reason string) string {
switch source {
case SourceOpenSSF:
return ReasonOpenSSF + " (OpenSSF)"
case SourceBlockList:
return ReasonBlockList
case SourceRemote:
if strings.TrimSpace(reason) == "" {
return ReasonRemote
}
return ReasonRemote + ": " + reason
default:
if strings.TrimSpace(reason) == "" {
return ReasonOpenSSF + " (OpenSSF)"
}
return reason
}
}

// ToastTitle picks the notify-block title from sources in a batch.
func ToastTitle(sources []string) string {
ossf, local, remote, other := false, false, false, false
for _, s := range sources {
switch s {
case SourceOpenSSF:
ossf = true
case SourceBlockList:
local = true
case SourceRemote:
remote = true
default:
other = true
}
}
if ossf && !local && !remote && !other {
return "Malicious package blocked"
}
if local && !ossf && !remote && !other {
return "Organization malware block"
}
return "Package blocked"
}
Loading
Loading