Skip to content

Commit

Permalink
add VirusTotal lookup provider
Browse files Browse the repository at this point in the history
  • Loading branch information
jondot committed May 22, 2021
1 parent d4b3b13 commit b8a02e8
Show file tree
Hide file tree
Showing 41 changed files with 4,591 additions and 57 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,39 @@ Information:
More: malshare.current.sha256.txt
```
## VirusTotal Lookup

You can use the [virus total community]() API access to lookup your hashes.



* Set `PF_VT_TOKEN=your-virustotal-api-key`

With this configured `preflight` will automatically create the VirusTotal lookup provider and validate digest with it.


Here is a full example for your CI, combining `preflight` with VirusTotal:

```
env:
PF_VT_TOKEN: {{secrets.PF_VT_TOKEN}}
steps:
- curl https://... | preflight <sha>
```


**Result:**

```
$ PF_VT_TOKEN=xxx preflight check e86d4eb1e888bd625389f2e50644be67a6bdbd77ff3bceaaf182d45860b88d80 kx-leecher.exe
⌛️ Preflight starting using VirusTotal
❌ Preflight failed: Digest matches but marked as vulnerable.
Information:
Vulnerability: VirusTotal stats - malicious: 40, suspicious 0
More: https://www.virustotal.com/gui/file/e86d4eb1e888bd625389f2e50644be67a6bdbd77ff3bceaaf182d45860b88d80/detection
```
## Other lookup types?

We've established that a _file lookup_ is universal and general enough to be useful to everyone. However, you might prefer your own vendor, or a service such as VirusTotal -- `preflight`'s architecture is pluggable and we're accepting [pull requests](https://github.com/spectralops/preflight).
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/spectralops/preflight
go 1.15

require (
github.com/VirusTotal/vt-go v0.0.0-20210415081439-66c331640a9d
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38
github.com/alecthomas/colour v0.1.0 // indirect
github.com/alecthomas/kong v0.2.15
Expand All @@ -11,7 +12,6 @@ require (
github.com/kr/pretty v0.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/stretchr/testify v1.6.1 // indirect
github.com/thoas/go-funk v0.8.0
golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46 // indirect
)
8 changes: 6 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/VirusTotal/vt-go v0.0.0-20210415081439-66c331640a9d h1:3TT/bNnjEfMysjRz822gkw2ca8LmAjgk9vc8FWhe7MU=
github.com/VirusTotal/vt-go v0.0.0-20210415081439-66c331640a9d/go.mod h1:u1+HeRyl/gQs67eDgVEWNE7+x+zCyXhdtNVrRJR5YPE=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/colour v0.1.0 h1:nOE9rJm6dsZ66RGWYSFrXw461ZIt9A6+nHgL7FRrDUk=
Expand Down Expand Up @@ -31,8 +33,10 @@ github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNX
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0=
github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs=
github.com/thoas/go-funk v0.8.0 h1:JP9tKSvnpFVclYgDM0Is7FD9M4fhPvqA0s0BsXmzSRQ=
github.com/thoas/go-funk v0.8.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down
4 changes: 2 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ import (

var CLI struct {
Run struct {
Hash string `arg name:"hash|url" help:"Hash to verify. You can provide a list seperated by a comma (,) and no space. Format: sha256=<hash>[,sha256=<hash2>,...], Or a URL to a flat file to fetch with a list of hashes, one per line. Format: https://example.com/file.txt"`
Hash string `arg name:"hash|url" help:"Hash to verify. You can provide a list separated by a comma (,) and no space. Format: sha256=<hash>[,sha256=<hash2>,...], Or a URL to a flat file to fetch with a list of hashes, one per line. Format: https://example.com/file.txt"`
Cmd []string `arg optional name:"cmd" help:"Command to execute"`
} `cmd help:"Verify and run a command"`

Check struct {
Hash string `arg name:"hash|url" help:"Hash to verify. You can provide a list seperated by a comma (,) and no space. Format: sha256=<hash>[,sha256=<hash2>,...], Or a URL to a flat file to fetch with a list of hashes, one per line. Format: https://example.com/file.txt"`
Hash string `arg name:"hash|url" help:"Hash to verify. You can provide a list separated by a comma (,) and no space. Format: sha256=<hash>[,sha256=<hash2>,...], Or a URL to a flat file to fetch with a list of hashes, one per line. Format: https://example.com/file.txt"`
Cmd []string `arg optional name:"cmd" help:"Command to execute"`
} `cmd help:"Verify a command"`

Expand Down
68 changes: 42 additions & 26 deletions pkg/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func (c *CheckResult) HasLookupVulns() bool {
}

type Lookup interface {
Hash(digest Digest) LookupResult
Hash(digest Digest) (LookupResult, error)
Name() string
}

Expand All @@ -90,16 +90,37 @@ type LookupResult struct {
}

type Preflight struct {
Lookup Lookup
Lookup []Lookup
Porcelain *Porcelain
}

func GetLookup() (Lookup, error) {
func GetLookup() ([]Lookup, error) {
// base lookup is always "no lookup"
lookups := []Lookup{&NoLookup{}}

//
// add: file list lookup
//
if os.Getenv("PF_FILE_LOOKUP") != "" {
return NewFileLookup(os.Getenv("PF_FILE_LOOKUP"))
lu, err := NewFileLookup(os.Getenv("PF_FILE_LOOKUP"))
if err != nil {
return nil, err
}
lookups = append(lookups, lu)
}

//
// add: virustotal lookup
//
if os.Getenv("PF_VT_TOKEN") != "" {
lu, err := NewVirusTotalLookup(os.Getenv("PF_VT_TOKEN"))
if err != nil {
return nil, err
}
lookups = append(lookups, lu)
}

return &NoLookup{}, nil
return lookups, nil
}

func createDigest(s string) Digest {
Expand All @@ -122,13 +143,7 @@ func createSignature(sig string) Signature {
return signature
}

func digestTuple(s, sig string) (Digest, Signature) {
signature := createSignature(sig)
digest := createDigest(s)
return digest, signature
}

func NewPreflight(lookup Lookup) *Preflight {
func NewPreflight(lookup []Lookup) *Preflight {
return &Preflight{
Lookup: lookup,
Porcelain: &Porcelain{},
Expand All @@ -138,7 +153,7 @@ func NewPreflight(lookup Lookup) *Preflight {
func (a *Preflight) Check(script, siglist string) (*CheckResult, error) {
sigs, err := parsehashList(siglist)
if err != nil {
return nil, err
return nil, fmt.Errorf("cannot parse hashlist: %v", err)
}
digest := createDigest(script)

Expand All @@ -156,23 +171,24 @@ func (a *Preflight) Check(script, siglist string) (*CheckResult, error) {
}

validDigest := res.(Signature)
// parseHashlist(sig) -> []Signature
// check should return err, wired by parseHashlist + hash lookup
// XXX untangle the digest tuple:
// createDigest
// parseSignature, accept sig []string
// "verify" a signature
// "validate" a hash
// 0. get digest object
// 1. parse all digs, then verify them. if one passes, we're OK
// 2. next, the one that passes we want to lookup
lookup := a.Lookup.Hash(digest)
var lookupResult LookupResult
for li := range a.Lookup {
lookup := a.Lookup[li]
lookupResult, err = lookup.Hash(digest)
if err != nil {
return nil, fmt.Errorf("%v - cannot look up: %v", lookup.Name(), err)
}
if lookupResult.Vulnerable {
break
}
}

return &CheckResult{
ExpectedDigests: sigs,
ActualDigest: digest,
ValidDigest: &validDigest,
LookupResult: &lookup,
Ok: !lookup.Vulnerable,
LookupResult: &lookupResult,
Ok: !lookupResult.Vulnerable,
}, nil
}

Expand Down
32 changes: 16 additions & 16 deletions pkg/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,31 @@ import (
)

func TestGoodMD5Check(t *testing.T) {
pf := NewPreflight(&NoLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}})
res, err := pf.Check("echo 'hello'", "md5=a849639cc38d82e3c0ac4e4dfd8186dd")
assert.NoError(t, err)
assert.Equal(t, true, res.Ok)
}
func TestGoodSHA1Check(t *testing.T) {
pf := NewPreflight(&NoLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}})
res, err := pf.Check("echo 'hello'", "sha1=098f8f78f1e13e2a2eee10d6974daebf892e4a71")
assert.NoError(t, err)
assert.Equal(t, true, res.Ok)
}
func TestGoodSHA256Check(t *testing.T) {
pf := NewPreflight(&NoLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}})
res, err := pf.Check("echo 'hello'", "sha256=3b084aa6ad2246428c9270825d8631e077b7e7c9bb16f6cafb482bc7fd63e348")
assert.NoError(t, err)
assert.Equal(t, true, res.Ok)
}
func TestGoodSHA256DefaultCheck(t *testing.T) {
pf := NewPreflight(&NoLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}})
res, err := pf.Check("echo 'hello'", "3b084aa6ad2246428c9270825d8631e077b7e7c9bb16f6cafb482bc7fd63e348")
assert.NoError(t, err)
assert.Equal(t, true, res.Ok)
}
func TestBadCheck(t *testing.T) {
pf := NewPreflight(&NoLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}})
res, err := pf.Check("abcd", "123")
assert.NoError(t, err)
sig := "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589"
Expand All @@ -43,7 +43,7 @@ func TestBadCheck(t *testing.T) {
}

func TestGoodCheck(t *testing.T) {
pf := NewPreflight(&NoLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}})
sig := "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589"
res, err := pf.Check("abcd", sig)
assert.NoError(t, err)
Expand All @@ -61,11 +61,11 @@ func (f *FakeLookup) Name() string {
return "Fake"
}

func (f *FakeLookup) Hash(digest Digest) LookupResult {
return LookupResult{Vulnerable: true, Message: "vuln", Link: "https://example.com/1"}
func (f *FakeLookup) Hash(digest Digest) (LookupResult, error) {
return LookupResult{Vulnerable: true, Message: "vuln", Link: "https://example.com/1"}, nil
}
func TestVulnerableCheck(t *testing.T) {
pf := NewPreflight(&FakeLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}, &FakeLookup{}})
sig := "88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589"
res, err := pf.Check("abcd", sig)
assert.NoError(t, err)
Expand All @@ -79,7 +79,7 @@ func TestVulnerableCheck(t *testing.T) {
assert.Equal(t, false, res.Ok)
}
func ExampleExecBadDigest() {
pf := NewPreflight(&FakeLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}, &FakeLookup{}})
pf.Exec([]string{"../test.sh"}, "123")

// Output:
Expand All @@ -96,7 +96,7 @@ func ExampleExecBadDigest() {
}

func ExampleExecVuln() {
pf := NewPreflight(&FakeLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}, &FakeLookup{}})
pf.Exec([]string{"../test.sh"}, "fe6d02cf15642ff8d5f61cad6d636a62fd46a5e5a49c06733fece838f5fa9d85")
// Output:
// ⌛️ Preflight starting with Fake
Expand All @@ -108,7 +108,7 @@ func ExampleExecVuln() {
}

func ExampleExecOk() {
pf := NewPreflight(&NoLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}})
pf.Exec([]string{"../test.sh"}, "fe6d02cf15642ff8d5f61cad6d636a62fd46a5e5a49c06733fece838f5fa9d85")

// Output:
Expand All @@ -118,7 +118,7 @@ func ExampleExecOk() {
}

func ExampleExecPipedBadDigest() {
pf := NewPreflight(&FakeLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}, &FakeLookup{}})
pf.ExecPiped("echo 'hello'", "123")

// Output:
Expand All @@ -135,7 +135,7 @@ func ExampleExecPipedBadDigest() {
}

func ExampleExecPipedVuln() {
pf := NewPreflight(&FakeLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}, &FakeLookup{}})
pf.ExecPiped("echo 'hello'", "3b084aa6ad2246428c9270825d8631e077b7e7c9bb16f6cafb482bc7fd63e348")
// Output:
// ⌛️ Preflight starting with Fake
Expand All @@ -147,7 +147,7 @@ func ExampleExecPipedVuln() {
}

func ExampleExecPipedOk() {
pf := NewPreflight(&NoLookup{})
pf := NewPreflight([]Lookup{&NoLookup{}})
pf.ExecPiped("echo 'hello'", "3b084aa6ad2246428c9270825d8631e077b7e7c9bb16f6cafb482bc7fd63e348")

// Output:
Expand All @@ -158,7 +158,7 @@ func ExampleExecPipedOk() {

func ExampleFileLookup() {
lookup, _ := NewFileLookup("../file_lookup_list.txt")
pf := NewPreflight(lookup)
pf := NewPreflight([]Lookup{&NoLookup{}, lookup})
pf.ExecPiped("echo 'hello'", "3b084aa6ad2246428c9270825d8631e077b7e7c9bb16f6cafb482bc7fd63e348")

// Output:
Expand Down
6 changes: 3 additions & 3 deletions pkg/file_lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func NewFileLookup(f string) (Lookup, error) {
func (p *FileLookup) Name() string {
return fmt.Sprintf("file lookup: %v", p.file)
}
func (p *FileLookup) Hash(digest Digest) LookupResult {
func (p *FileLookup) Hash(digest Digest) (LookupResult, error) {
if strings.Contains(p.content, digest.SHA1) ||
strings.Contains(p.content, digest.MD5) ||
strings.Contains(p.content, digest.SHA256) {
Expand All @@ -36,7 +36,7 @@ func (p *FileLookup) Hash(digest Digest) LookupResult {
Vulnerable: true,
Message: "Hash was found in a vulnerable digest list",
Link: p.file,
}
}, nil
}
return LookupResult{}
return LookupResult{}, nil
}
4 changes: 2 additions & 2 deletions pkg/no_lookup.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ func (p *NoLookup) Name() string {
return ""
}

func (p *NoLookup) Hash(digest Digest) LookupResult {
return LookupResult{Vulnerable: false}
func (p *NoLookup) Hash(digest Digest) (LookupResult, error) {
return LookupResult{Vulnerable: false}, nil
}
12 changes: 9 additions & 3 deletions pkg/porcelain.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ func fmtSigs(sigs []Signature) string {
}).([]string), ", ")
}

func fmtLookups(sigs []Lookup) string {
return strings.Join(funk.Map(sigs, func(lk Lookup) string {
return lk.Name()
}).([]string), ", ")
}

func (p *Porcelain) Start(pf *Preflight) {
name := pf.Lookup.Name()
if name != "" {
name = fmt.Sprintf(" with %s", name)
name := ""
if len(pf.Lookup) > 1 { // first item is the empty lookup
name = fmt.Sprintf(" with %s", fmtLookups(pf.Lookup[1:]))
}
fmt.Printf("%v Preflight starting%v\n", EMO_TIME, name)
}
Expand Down
Loading

0 comments on commit b8a02e8

Please sign in to comment.