-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstore.go
More file actions
114 lines (106 loc) · 3.29 KB
/
store.go
File metadata and controls
114 lines (106 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
package store
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/NextronSystems/jsonlog/thorlog/v3"
)
func New(root string) *Store {
return &Store{
RootDir: root,
Flat: false,
}
}
type Store struct {
RootDir string
Flat bool
}
const (
subdirFindings = "findings"
subdirContent = "samples"
suffixMetadata = ".metadata"
suffixHash = ".hash"
)
func (s *Store) Store(finding *thorlog.Assessment, content io.ReadSeeker) error {
findingId := finding.Meta.GenID
if findingId == "" {
return fmt.Errorf("finding ID is empty, cannot store finding")
} else if len(findingId) < 2 {
return fmt.Errorf("finding ID is too short, must be at least 2 characters: %s", findingId)
}
var contentHash string
if content != nil {
// Shortcut: if the content is already hashed, we can use it directly.
if file, isFile := finding.Subject.(*thorlog.File); isFile && file.Hashes != nil {
contentHash = file.Hashes.Sha256
} else {
hash := sha256.New()
if _, err := io.Copy(hash, content); err != nil {
return fmt.Errorf("could not hash content: %w", err)
}
contentHash = hex.EncodeToString(hash.Sum(nil))
// Reset the content reader to the beginning for later use.
if _, err := content.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("cannot reset content reader: %w", err)
}
}
}
findingJson, err := json.Marshal(finding)
if err != nil {
return fmt.Errorf("cannot marshal finding: %w", err)
}
if err := s.storeData(subdirFindings, findingId, bytes.NewReader(findingJson), false); err != nil {
return fmt.Errorf("cannot store finding data: %w", err)
}
if content != nil {
if err := s.storeData(subdirContent, contentHash, content, false); err != nil {
if !os.IsExist(err) { // If the content already exists, we can ignore the error.
return fmt.Errorf("cannot store content data: %w", err)
}
}
// Store cross-references: Finding ID -> content hash, and content hash -> finding metadata.
// A finding can have only one content hash, but a content hash can be referenced by multiple findings.
if err := s.storeData(subdirFindings, findingId+suffixHash, strings.NewReader(contentHash), false); err != nil {
return fmt.Errorf("cannot store content hash for finding: %w", err)
}
if err := s.storeData(subdirContent, contentHash+suffixMetadata, bytes.NewReader(append(findingJson, '\n')), true); err != nil {
return fmt.Errorf("cannot store content metadata: %w", err)
}
}
return nil
}
func (s *Store) storeData(subdir string, id string, data io.Reader, append bool) error {
path := s.path(subdir, id)
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Errorf("cannot create directory: %w", err)
}
var openFlags = os.O_WRONLY | os.O_CREATE
if append {
openFlags |= os.O_APPEND
} else {
openFlags |= os.O_EXCL
}
file, err := os.OpenFile(path, openFlags, 0644)
if err != nil {
return fmt.Errorf("cannot create file: %w", err)
}
defer func() {
_ = file.Close()
}()
if _, err := io.Copy(file, data); err != nil {
return fmt.Errorf("cannot write to file: %w", err)
}
return nil
}
func (s *Store) path(subdir string, id string) string {
if s.Flat {
return filepath.Join(s.RootDir, subdir, id)
}
return filepath.Join(s.RootDir, subdir, id[:2], id)
}