Repository navigation
Expand file tree
/
Copy pathsam_codeuri_test.go
More file actions
143 lines (135 loc) · 5.53 KB
/
Copy pathsam_codeuri_test.go
File metadata and controls
143 lines (135 loc) · 5.53 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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package main
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
// samCodeUriRe finds a `CodeUri:` property in a SAM template. Deliberately a
// regex over the raw YAML rather than a parse: these templates use CFN
// shorthand (!Sub, !Ref, !GetAtt), which a plain YAML unmarshaller rejects, and
// pulling in a CFN-aware parser to assert one string is not worth it.
var samCodeUriRe = regexp.MustCompile(`(?m)^\s*CodeUri:\s*(\S+)\s*$`)
// samTemplates returns every lambda/*/template.yaml that declares a serverless
// function, i.e. the ones `sam deploy` will package.
func samTemplates(t *testing.T) map[string]string {
t.Helper()
found := map[string]string{}
matches, err := filepath.Glob("lambda/*/template.yaml")
if err != nil {
t.Fatalf("glob templates: %v", err)
}
for _, p := range matches {
b, err := os.ReadFile(p)
if err != nil {
t.Fatalf("read %s: %v", p, err)
}
body := string(b)
if !strings.Contains(body, "AWS::Serverless::Function") {
continue
}
found[p] = body
}
if len(found) == 0 {
t.Skip("no SAM templates found")
}
return found
}
// TestSamCodeUriIsADedicatedBuildDir guards spawn#645.
//
// `sam deploy` zips the ENTIRE CodeUri directory with no filtering whatsoever —
// samcli/lib/package/utils.py walks it with os.walk and adds every file,
// dotfiles included. There is no `.samignore` feature (no such string anywhere in
// SAM CLI 1.166.2) and `.gitignore` is not consulted, so #637's gitignore work
// does nothing for deployment.
//
// With `CodeUri: .` that shipped the stray dev binary, a nested function.zip
// containing a second copy of bootstrap, and every source file: the ttl-reaper
// deployed at 75.7 MiB against a 25.2 MiB real artifact.
//
// A missing CodeUri is worse, not better: ServerlessFunctionResource inherits
// PACKAGE_NULL_PROPERTY = True, and upload_local_artifacts' own docstring says
// "If path is omitted, this method will zip the current working folder and
// upload" — so it packaged whatever cwd happened to be.
func TestSamCodeUriIsADedicatedBuildDir(t *testing.T) {
for path, body := range samTemplates(t) {
m := samCodeUriRe.FindStringSubmatch(body)
if m == nil {
t.Errorf("%s declares a serverless function but sets no CodeUri.\n"+
"That is not a safe default: SAM packages the CURRENT WORKING DIRECTORY when "+
"CodeUri is omitted, so what ships depends on where make was run. Set "+
"`CodeUri: build/`.", path)
continue
}
switch got := m[1]; got {
case ".", "./":
t.Errorf("%s sets CodeUri: %s — SAM zips that whole directory with no filtering "+
"(dotfiles included, no .samignore), so the dev binary, function.zip and every "+
"source file ship with it. Point it at a dedicated build/ dir holding only "+
"bootstrap.", path, got)
case "build/", "build":
// Correct.
default:
t.Errorf("%s sets CodeUri: %s — expected build/. If this is intentional, the "+
"directory must still contain nothing but the Lambda artifact.", path, got)
}
}
}
// TestSamCodeUriDirHoldsOnlyBootstrap is the assertion that actually bounds the
// upload. The CodeUri value only says WHERE SAM looks; this says what it will
// find. It is skipped when the directory has not been built, so it is advisory
// in a clean checkout and binding on any machine that has run `make build` —
// which is every machine that could deploy.
func TestSamCodeUriDirHoldsOnlyBootstrap(t *testing.T) {
for path := range samTemplates(t) {
dir := filepath.Join(filepath.Dir(path), "build")
entries, err := os.ReadDir(dir)
if err != nil {
continue // not built here; nothing to bound
}
var names []string
for _, e := range entries {
names = append(names, e.Name())
}
if len(names) != 1 || names[0] != "bootstrap" {
t.Errorf("%s contains %v, want exactly [bootstrap].\n"+
"Every file here is uploaded into the Lambda deployment package.", dir, names)
}
}
}
// TestDeployParamsNeverInsideCodeUri is the security half, and the one most
// likely to outlive anyone's memory of why it exists.
//
// lambda/ttl-reaper/Makefile generates .deploy-params.yaml immediately before
// `sam deploy`, holding every deploy parameter including NotifyUrl — which that
// Makefile's own comment calls "a Slack webhook secret". SAM's walk includes
// dotfiles, so had that file been inside the CodeUri directory it would be
// uploaded into the deployment package and readable by anyone with
// lambda:GetFunction.
//
// It must stay in the module root, NOT in build/.
func TestDeployParamsNeverInsideCodeUri(t *testing.T) {
for path := range samTemplates(t) {
moduleDir := filepath.Dir(path)
buildDir := filepath.Join(moduleDir, "build")
if _, err := os.Stat(filepath.Join(buildDir, ".deploy-params.yaml")); err == nil {
t.Errorf("%s/.deploy-params.yaml is inside the CodeUri directory — it holds deploy "+
"parameters including a Slack webhook (NotifyUrl) and SAM uploads dotfiles, so "+
"this would ship the secret in the deployment package.", buildDir)
}
// The Makefile's PARAMS_FILE must not be pointed into build/ either —
// catching the regression at its source rather than after a deploy.
mk, err := os.ReadFile(filepath.Join(moduleDir, "Makefile"))
if err != nil {
continue
}
for _, line := range strings.Split(string(mk), "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "PARAMS_FILE") && strings.Contains(trimmed, "build/") {
t.Errorf("%s/Makefile points PARAMS_FILE into build/ (%q). That directory is "+
"uploaded to Lambda; the params file holds a webhook secret.", moduleDir, trimmed)
}
}
}
}