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
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@ jobs:
run: |
go env -w GOFLAGS=-mod=mod
go build -v ./...

- name: Vet
run: go vet ./...

- name: Test
run: go test -race -v ./...
117 changes: 117 additions & 0 deletions cmd/scan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright 2018 Saferwall. All rights reserved.
// Use of this source code is governed by Apache v2 license
// license that can be found in the LICENSE file.

package cmd

import (
"os"
"path/filepath"
"reflect"
"testing"

"github.com/saferwall/cli/internal/entity"
)

const testHash = "275a021bbfb6489e54d471899f7db9d1663fc695ec2fe2a2c4538aabf651fd0f"

func TestBuildScanSummary(t *testing.T) {
success := true
file := entity.File{
SHA256: testHash,
Size: 2048,
Classification: "Ransomware.Wannacry",
Format: "pe",
Extension: "exe",
Encrypted: true,
DecryptionSuccess: &success,
SuccessfulPassword: "infected",
AttemptedPasswords: []string{"infected", "malware"},
MultiAV: map[string]any{
"last_scan": map[string]any{
"stats": map[string]any{
"positives": float64(12),
"engines_count": float64(14),
},
},
},
}

got := buildScanSummary(file)

if got.SHA256 != testHash {
t.Errorf("sha256 = %q, want %q", got.SHA256, testHash)
}
if got.Size != 2048 {
t.Errorf("size = %d, want 2048", got.Size)
}
if got.Classification != "Ransomware.Wannacry" {
t.Errorf("classification = %q, want Ransomware.Wannacry", got.Classification)
}
if got.FileFormat != "pe" || got.FileExtension != "exe" {
t.Errorf("format/extension = %q/%q, want pe/exe", got.FileFormat, got.FileExtension)
}
if !got.Encrypted || got.DecryptionSuccess == nil || !*got.DecryptionSuccess {
t.Error("encryption fields not carried over")
}
if got.SuccessfulPassword != "infected" {
t.Errorf("successful_password = %q, want infected", got.SuccessfulPassword)
}
if got.MultiAV == nil {
t.Fatal("multiav summary = nil, want populated")
}
if got.MultiAV.Positives != 12 || got.MultiAV.EnginesCount != 14 {
t.Errorf("multiav = %d/%d, want 12/14", got.MultiAV.Positives, got.MultiAV.EnginesCount)
}
}

func TestBuildScanSummaryNoMultiAV(t *testing.T) {
got := buildScanSummary(entity.File{SHA256: testHash})
if got.MultiAV != nil {
t.Errorf("multiav = %+v, want nil when last_scan stats are absent", got.MultiAV)
}
}

func TestSha256Re(t *testing.T) {
tests := []struct {
input string
want bool
}{
{testHash, true},
{"275A021BBFB6489E54D471899F7DB9D1663FC695EC2FE2A2C4538AABF651FD0F", true},
{"not-a-hash", false},
{testHash[:63], false},
{testHash + "0", false},
{"", false},
}
for _, tt := range tests {
if got := sha256Re.MatchString(tt.input); got != tt.want {
t.Errorf("sha256Re.MatchString(%q) = %v, want %v", tt.input, got, tt.want)
}
}
}

func TestCollectHashesSingleHash(t *testing.T) {
got := collectHashes(testHash)
if !reflect.DeepEqual(got, []string{testHash}) {
t.Errorf("collectHashes() = %v, want [%s]", got, testHash)
}
}

func TestCollectHashesFromFile(t *testing.T) {
other := "0000000000000000000000000000000000000000000000000000000000000000"
path := filepath.Join(t.TempDir(), "hashes.txt")
content := testHash + "\n" +
" " + other + " \n" + // surrounding whitespace is trimmed
"garbage line\n" +
"\n"
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}

got := collectHashes(path)
want := []string{testHash, other}
if !reflect.DeepEqual(got, want) {
t.Errorf("collectHashes() = %v, want %v", got, want)
}
}
162 changes: 162 additions & 0 deletions internal/util/utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright 2022 Saferwall. All rights reserved.
// Use of this source code is governed by Apache v2 license
// license that can be found in the LICENSE file.

package util

import (
"bytes"
"os"
"path/filepath"
"reflect"
"testing"
)

func TestGetSha256(t *testing.T) {
tests := []struct {
name string
input []byte
want string
}{
{
name: "empty input",
input: []byte{},
want: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
},
{
name: "known vector",
input: []byte("abc"),
want: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetSha256(tt.input); got != tt.want {
t.Errorf("GetSha256() = %s, want %s", got, tt.want)
}
})
}
}

func TestReadAll(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "sample.bin")
content := bytes.Repeat([]byte("saferwall"), 1000)
if err := os.WriteFile(path, content, 0644); err != nil {
t.Fatal(err)
}

got, err := ReadAll(path)
if err != nil {
t.Fatalf("ReadAll() error = %v", err)
}
if !bytes.Equal(got, content) {
t.Errorf("ReadAll() returned %d bytes, want %d", len(got), len(content))
}
}

func TestReadAllMissingFile(t *testing.T) {
if _, err := ReadAll(filepath.Join(t.TempDir(), "nope")); err == nil {
t.Error("ReadAll() expected error for missing file, got nil")
}
}

func TestWriteBytesFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "out.bin")
content := []byte("hello saferwall")

n, err := WriteBytesFile(path, bytes.NewReader(content))
if err != nil {
t.Fatalf("WriteBytesFile() error = %v", err)
}
if n != len(content) {
t.Errorf("WriteBytesFile() wrote %d bytes, want %d", n, len(content))
}

got, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, content) {
t.Errorf("file content = %q, want %q", got, content)
}
}

func TestExists(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "file.txt")
if err := os.WriteFile(path, []byte("x"), 0644); err != nil {
t.Fatal(err)
}

if !Exists(path) {
t.Errorf("Exists(%q) = false, want true", path)
}
if !Exists(dir) {
t.Errorf("Exists(%q) = false, want true for directory", dir)
}
if Exists(filepath.Join(dir, "missing")) {
t.Error("Exists() = true for missing file, want false")
}
}

func TestMkDir(t *testing.T) {
dir := filepath.Join(t.TempDir(), "newdir")

if !MkDir(dir) {
t.Fatalf("MkDir(%q) = false, want true", dir)
}
if !Exists(dir) {
t.Errorf("MkDir(%q) did not create the directory", dir)
}
// Calling it again on an existing directory succeeds.
if !MkDir(dir) {
t.Errorf("MkDir(%q) = false on existing directory, want true", dir)
}
}

func TestStringInSlice(t *testing.T) {
list := []string{"a", "b", "c"}
if !StringInSlice("b", list) {
t.Error(`StringInSlice("b") = false, want true`)
}
if StringInSlice("z", list) {
t.Error(`StringInSlice("z") = true, want false`)
}
if StringInSlice("a", nil) {
t.Error("StringInSlice() on nil slice = true, want false")
}
}

func TestUniqueSlice(t *testing.T) {
got := UniqueSlice([]string{"a", "b", "a", "c", "b"})
want := []string{"a", "b", "c"}
if !reflect.DeepEqual(got, want) {
t.Errorf("UniqueSlice() = %v, want %v", got, want)
}
}

func TestWalkAllFilesInDir(t *testing.T) {
dir := t.TempDir()
sub := filepath.Join(dir, "sub")
if err := os.Mkdir(sub, 0755); err != nil {
t.Fatal(err)
}
for _, f := range []string{
filepath.Join(dir, "a.txt"),
filepath.Join(sub, "b.txt"),
} {
if err := os.WriteFile(f, []byte("x"), 0644); err != nil {
t.Fatal(err)
}
}

got, err := WalkAllFilesInDir(dir)
if err != nil {
t.Fatalf("WalkAllFilesInDir() error = %v", err)
}
want := []string{filepath.Join(dir, "a.txt"), filepath.Join(sub, "b.txt")}
if !reflect.DeepEqual(got, want) {
t.Errorf("WalkAllFilesInDir() = %v, want %v", got, want)
}
}
Loading