Repository: ossf/package-analysis (go)

Issue:
Parse unlink from strace output to capture file deletion 
Currently we don't record when "unlink" is run during execution.

This may be helpful when detecting malicious activity.

Pull request:
add unlink/unlinkat parsing to strace.go, and add Delete field to FileInfo
Fixes #250 

Patch:
diff --git a/function/loader/schema.json b/function/loader/schema.json
index 15667fa..631af4e 100644
--- a/function/loader/schema.json
+++ b/function/loader/schema.json
@@ -78,6 +78,11 @@
           },
           {
             "fields": [
+              {
+                "mode": "NULLABLE",
+                "name": "Delete",
+                "type": "BOOLEAN"
+              },
               {
                 "mode": "NULLABLE",
                 "name": "Write",
diff --git a/internal/analysis/analysis.go b/internal/analysis/analysis.go
index 5ee660e..b0f395c 100644
--- a/internal/analysis/analysis.go
+++ b/internal/analysis/analysis.go
@@ -57,9 +57,10 @@ func statusForRunResult(r *sandbox.RunResult) status {
 }
 
 type fileResult struct {
-	Path  string
-	Read  bool
-	Write bool
+	Path   string
+	Read   bool
+	Write  bool
+	Delete bool
 }
 
 type socketResult struct {
@@ -147,9 +148,10 @@ func Run(sb sandbox.Sandbox, args []string) (*Result, error) {
 func (d *Result) setData(straceResult *strace.Result, dns *dnsanalyzer.DNSAnalyzer) {
 	for _, f := range straceResult.Files() {
 		d.Files = append(d.Files, fileResult{
-			Path:  f.Path,
-			Read:  f.Read,
-			Write: f.Write,
+			Path:   f.Path,
+			Read:   f.Read,
+			Write:  f.Write,
+			Delete: f.Delete,
 		})
 	}
 
diff --git a/internal/strace/strace.go b/internal/strace/strace.go
index 08c5e36..ad0c809 100644
--- a/internal/strace/strace.go
+++ b/internal/strace/strace.go
@@ -35,12 +35,19 @@ var (
 	// 0x3 socket:[4], 0x55ed873bb510 {Family: AF_INET6, Addr: 2001:67c:1360:8001::24, Port: 80}, 0x1c
 	// 0x3 socket:[16], 0x5568c5caf2d0 {Family: AF_INET, Addr: , Port: 5000}, 0x10
 	socketPattern = regexp.MustCompile(`{Family: ([^,]+), (Addr: ([^,]*), Port: ([0-9]+)|[^}]+)}`)
+
+	// 0x7fe003272980 /tmp/jpu6po61
+	unlinkPatten = regexp.MustCompile(`0x[a-f\d]+ ([^)]+)`)
+
+	// unlinkat(0x4 /tmp/pip-pip-egg-info-ng4_5gp_/temps.egg-info, 0x7fe0031c9a10 top_level.txt, 0x0)
+	unlinkatPattern = regexp.MustCompile(`0x[a-f\d]+ ([^,]+), 0x[a-f\d]+ ([^,]+), 0x[a-f\d]+`)
 )
 
 type FileInfo struct {
-	Path  string
-	Read  bool
-	Write bool
+	Path   string
+	Read   bool
+	Write  bool
+	Delete bool
 }
 
 type SocketInfo struct {
@@ -112,12 +119,13 @@ func parseCmdAndEnv(cmdAndEnv string) ([]string, []string, error) {
 	return cmd, env, nil
 }
 
-func (r *Result) recordFileAccess(file string, read, write bool) {
+func (r *Result) recordFileAccess(file string, read, write, delete bool) {
 	if _, exists := r.files[file]; !exists {
 		r.files[file] = &FileInfo{Path: file}
 	}
 	r.files[file].Read = r.files[file].Read || read
 	r.files[file].Write = r.files[file].Write || write
+	r.files[file].Delete = r.files[file].Delete || delete
 }
 
 func (r *Result) recordSocket(address string, port int) {
@@ -152,7 +160,7 @@ func (r *Result) parseSyscall(syscall, args string) error {
 
 		log.Debug("creat",
 			"path", match[1])
-		r.recordFileAccess(match[1], false, true)
+		r.recordFileAccess(match[1], false, true, false)
 	case "open":
 		match := openPattern.FindStringSubmatch(args)
 		if match == nil {
@@ -164,7 +172,7 @@ func (r *Result) parseSyscall(syscall, args string) error {
 			"path", match[1],
 			"read", read,
 			"write", write)
-		r.recordFileAccess(match[1], read, write)
+		r.recordFileAccess(match[1], read, write, false)
 	case "openat":
 		match := openatPattern.FindStringSubmatch(args)
 		if match == nil {
@@ -177,7 +185,7 @@ func (r *Result) parseSyscall(syscall, args string) error {
 			"path", path,
 			"read", read,
 			"write", write)
-		r.recordFileAccess(path, read, write)
+		r.recordFileAccess(path, read, write, false)
 	case "execve":
 		match := execvePattern.FindStringSubmatch(args)
 		if match == nil {
@@ -224,7 +232,7 @@ func (r *Result) parseSyscall(syscall, args string) error {
 		}
 		log.Debug("stat",
 			"path", match[1])
-		r.recordFileAccess(match[1], true, false)
+		r.recordFileAccess(match[1], true, false, false)
 	case "newfstatat":
 		match := newfstatatPattern.FindStringSubmatch(args)
 		if match == nil {
@@ -233,7 +241,19 @@ func (r *Result) parseSyscall(syscall, args string) error {
 		path := joinPaths(match[1], match[2])
 		log.Debug("newfstatat",
 			"path", path)
-		r.recordFileAccess(path, true, false)
+		r.recordFileAccess(path, true, false, false)
+	case "unlink":
+		match := unlinkPatten.FindStringSubmatch(args)
+		path := match[1]
+		log.Debug("unlink",
+			"path", path)
+		r.recordFileAccess(path, false, false, true)
+	case "unlinkat":
+		match := unlinkatPattern.FindStringSubmatch(args)
+		path := joinPaths(match[1], match[2])
+		log.Debug("unlinkat",
+			"path", path)
+		r.recordFileAccess(path, false, false, true)
 	}
 	return nil
 }


Test patch:
diff --git a/internal/strace/strace_test.go b/internal/strace/strace_test.go
index b36d19b..316a723 100644
--- a/internal/strace/strace_test.go
+++ b/internal/strace/strace_test.go
@@ -186,6 +186,22 @@ func TestParseFilesOneEntry(t *testing.T) {
 				Write: false,
 			},
 		},
+		{
+			name:  "unlink",
+			input: "I0902 01:19:17.729518     303 strace.go:625] [   4:   4] python3 X unlink(0x7ff5f78e4980 /tmp/lbosrzlp) = 0 (0x0) (58.552Âµs)",
+			want: strace.FileInfo{
+				Path:   "/tmp/lbosrzlp",
+				Delete: true,
+			},
+		},
+		{
+			name:  "unlinkat",
+			input: "I0902 01:19:18.991729     303 strace.go:631] [   4:   4] python3 X unlinkat(0x3 /tmp/pip-unpack-7xfj8327, 0x7ff5f790c410 temps-0.3.0.tar.gz, 0x0) = 0 (0x0) (39.914Âµs)",
+			want: strace.FileInfo{
+				Path:   "/tmp/pip-unpack-7xfj8327/temps-0.3.0.tar.gz",
+				Delete: true,
+			},
+		},
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {