mirror of
https://github.com/anchore/syft.git
synced 2026-10-25 03:22:22 +01:00
Compare commits
647
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
873f6e4037 | ||
|
|
3de5e98db1 | ||
|
|
41601e60ca | ||
|
|
0748413d6b | ||
|
|
b0ab75fd89 | ||
|
|
1bffc6ef0c | ||
|
|
f9a862d268 | ||
|
|
aec53bc32d | ||
|
|
75fdb9e623 | ||
|
|
29c9857857 | ||
|
|
d1e4ecba42 | ||
|
|
c209d03fe8 | ||
|
|
fecfb2f939 | ||
|
|
e61aac0b53 | ||
|
|
85cc64f7e3 | ||
|
|
1af68683d0 | ||
|
|
1ca8ee2a8d | ||
|
|
7182f5b519 | ||
|
|
a16a4ad6c9 | ||
|
|
3174a17efb | ||
|
|
bf39456fbc | ||
|
|
c72d295719 | ||
|
|
4c20a74d2f | ||
|
|
7c67df397e | ||
|
|
0e5fb8e01f | ||
|
|
8ea2425c97 | ||
|
|
2a04e06cbc | ||
|
|
3a6b6562d1 | ||
|
|
63e7a004cb | ||
|
|
04e8c96822 | ||
|
|
1bc4179193 | ||
|
|
51a1bad159 | ||
|
|
3cffa0b7fd | ||
|
|
da03e981c3 | ||
|
|
4aa2d8c0af | ||
|
|
a635d66657 | ||
|
|
51d3cd0066 | ||
|
|
4846639ee4 | ||
|
|
6030a69b17 | ||
|
|
8b9194eb81 | ||
|
|
56a1ab54d2 | ||
|
|
f4dd36ca9d | ||
|
|
17c605822e | ||
|
|
4eace4b141 | ||
|
|
05660da8d7 | ||
|
|
2f378d806e | ||
|
|
b83cc8485a | ||
|
|
38a12bd91a | ||
|
|
649d152548 | ||
|
|
f5d5892434 | ||
|
|
51831d303c | ||
|
|
09e62c3282 | ||
|
|
d39ef44e40 | ||
|
|
8bca0ac39e | ||
|
|
67dbd1fe4c | ||
|
|
402227f0b3 | ||
|
|
2bcf825857 | ||
|
|
9cb7c3d350 | ||
|
|
e789e0714d | ||
|
|
790ecc6f28 | ||
|
|
b9462db59e | ||
|
|
bfad9659a8 | ||
|
|
b345752f49 | ||
|
|
ba9dd1d5fd | ||
|
|
68f35815d6 | ||
|
|
4d4b502174 | ||
|
|
ea80f94c0e | ||
|
|
2c145f70b2 | ||
|
|
bcc7e90fcc | ||
|
|
fa1baabc05 | ||
|
|
b5906824cb | ||
|
|
16dee41b4b | ||
|
|
23778de112 | ||
|
|
3ba9df4ff3 | ||
|
|
bbf223b2c9 | ||
|
|
b126276f97 | ||
|
|
40d766a257 | ||
|
|
814960f65a | ||
|
|
13758260dd | ||
|
|
11039f4b4e | ||
|
|
413ffdb233 | ||
|
|
e53fe51612 | ||
|
|
facbc486a8 | ||
|
|
5d42a349e6 | ||
|
|
4adfbeb5f0 | ||
|
|
b943da6433 | ||
|
|
e8119acf93 | ||
|
|
06b9a79e3d | ||
|
|
ef5c1651ef | ||
|
|
5c8dd4c3a7 | ||
|
|
f5a6b5a02f | ||
|
|
c379d21e9a | ||
|
|
a50a0f77d2 | ||
|
|
ea4a6747eb | ||
|
|
1cfc4c7387 | ||
|
|
4d0da703bf | ||
|
|
4ee6be3777 | ||
|
|
5d44e49d2f | ||
|
|
5dd3b127b0 | ||
|
|
ce4b31757a | ||
|
|
c08b0990ca | ||
|
|
8ee209a5ae | ||
|
|
4712246897 | ||
|
|
ebeb768f59 | ||
|
|
d4733fac1d | ||
|
|
34774a0e10 | ||
|
|
1c582f0aa5 | ||
|
|
9d766c0325 | ||
|
|
5751b43608 | ||
|
|
51d015d5ea | ||
|
|
5565bdef0c | ||
|
|
1676934c63 | ||
|
|
7cfb5f630a | ||
|
|
ba80e490c2 | ||
|
|
1c787f436f | ||
|
|
c7eb3f4c93 | ||
|
|
11a8cde8e4 | ||
|
|
dcd062cffb | ||
|
|
b9294976ef | ||
|
|
3e8a2304e8 | ||
|
|
e04d90fc9a | ||
|
|
0f39917999 | ||
|
|
0652998b9b | ||
|
|
43bdf6e1b2 | ||
|
|
9aa9e0e09a | ||
|
|
7ccbadff34 | ||
|
|
3f13d209a5 | ||
|
|
baa3dc74d3 | ||
|
|
58f310c390 | ||
|
|
a383239217 | ||
|
|
0891d35e07 | ||
|
|
502971a1b2 | ||
|
|
dc14dbb326 | ||
|
|
bae5a2e741 | ||
|
|
220655743b | ||
|
|
9fce006b8f | ||
|
|
d91c2dd842 | ||
|
|
9b98785aab | ||
|
|
ad977ee0a1 | ||
|
|
9eac737fe2 | ||
|
|
4ba92ac43b | ||
|
|
a4b895d31f | ||
|
|
9fa11f2339 | ||
|
|
1470abaded | ||
|
|
2d582f78a1 | ||
|
|
793cef5086 | ||
|
|
b2f4d7eda2 | ||
|
|
6c41f15975 | ||
|
|
26cdbfc299 | ||
|
|
dc9bc58480 | ||
|
|
a6d73e5659 | ||
|
|
ee4e69285a | ||
|
|
59c56a5666 | ||
|
|
78ac2f9797 | ||
|
|
262423b1e8 | ||
|
|
1aaa644007 | ||
|
|
f442586ec9 | ||
|
|
12877ed863 | ||
|
|
629aafc323 | ||
|
|
f430788099 | ||
|
|
58850d3258 | ||
|
|
2428d704e1 | ||
|
|
da07520121 | ||
|
|
ae27dcdfa9 | ||
|
|
1daf18fee9 | ||
|
|
7392d607b6 | ||
|
|
7315f83f9d | ||
|
|
cd530924d0 | ||
|
|
73d5852119 | ||
|
|
c4b464e616 | ||
|
|
234ce4e1f3 | ||
|
|
f9433e7f9b | ||
|
|
5a4778093d | ||
|
|
bdbf927847 | ||
|
|
f3d95aa3a9 | ||
|
|
671ff39933 | ||
|
|
263be01faa | ||
|
|
8f6bdde666 | ||
|
|
ef43294d0e | ||
|
|
e1ad340c2d | ||
|
|
07f13049da | ||
|
|
6c7900f5b8 | ||
|
|
7018573bf7 | ||
|
|
f3ad8cf250 | ||
|
|
31f1d7dbf0 | ||
|
|
144ed725a7 | ||
|
|
dcec2bc352 | ||
|
|
1fe0921a5b | ||
|
|
538fe5ee1d | ||
|
|
2687100e6a | ||
|
|
fe7a417fb2 | ||
|
|
7732cd3b48 | ||
|
|
0302fc5b48 | ||
|
|
b899536814 | ||
|
|
d1120ad56e | ||
|
|
ef759038f5 | ||
|
|
0748945c83 | ||
|
|
185d0d1bfa | ||
|
|
d16ecdf715 | ||
|
|
68cf57ed03 | ||
|
|
f6c8057977 | ||
|
|
87e57aa925 | ||
|
|
eed35ec9ce | ||
|
|
b23879fd37 | ||
|
|
30f0686353 | ||
|
|
235ad2e749 | ||
|
|
127fac8ca9 | ||
|
|
37bb95f5c9 | ||
|
|
1067dc2ce9 | ||
|
|
21878784a8 | ||
|
|
86005d1593 | ||
|
|
45625dae94 | ||
|
|
38d5ef2c84 | ||
|
|
44e5480238 | ||
|
|
7b1af8721d | ||
|
|
8f57d22f63 | ||
|
|
351c683cb4 | ||
|
|
534a5f54b0 | ||
|
|
8a414b5366 | ||
|
|
e34adea749 | ||
|
|
8314c0d2cb | ||
|
|
b7fa75d7f8 | ||
|
|
7d0d3e1977 | ||
|
|
58f8c852df | ||
|
|
b8f52d570e | ||
|
|
ba00f3328d | ||
|
|
962ff1ec49 | ||
|
|
40899adb87 | ||
|
|
650f71cbe0 | ||
|
|
30885ed92e | ||
|
|
51243aa65f | ||
|
|
23e3de75e3 | ||
|
|
594ba5f295 | ||
|
|
5d48882a78 | ||
|
|
ec4d595920 | ||
|
|
094b41b301 | ||
|
|
2eb2d55551 | ||
|
|
a46d12270f | ||
|
|
3e16c6813f | ||
|
|
5035d9ca1a | ||
|
|
c21b16d924 | ||
|
|
4a2fc226dd | ||
|
|
7de5643227 | ||
|
|
9de4129638 | ||
|
|
3a45653cfa | ||
|
|
b82c0ffc34 | ||
|
|
b2be411f77 | ||
|
|
ec22f4b773 | ||
|
|
e3c525b4b8 | ||
|
|
3842d28e90 | ||
|
|
9f22ab6137 | ||
|
|
1315cfd787 | ||
|
|
212aa9b6cf | ||
|
|
46e4ac1474 | ||
|
|
6800a5f64b | ||
|
|
9caf51596e | ||
|
|
7645d5759d | ||
|
|
ce32f8bb74 | ||
|
|
f8ab7c4695 | ||
|
|
305ee87052 | ||
|
|
49e7f399f9 | ||
|
|
007b034ee3 | ||
|
|
b454160549 | ||
|
|
36d794febe | ||
|
|
51d38f8e59 | ||
|
|
cfebae27f5 | ||
|
|
2b7a9d0be3 | ||
|
|
b03e9c6868 | ||
|
|
cedfa05e93 | ||
|
|
dd09e0362e | ||
|
|
4ae94c37eb | ||
|
|
d08e2be768 | ||
|
|
faa902209e | ||
|
|
9a2a988e7f | ||
|
|
5ceef48949 | ||
|
|
1848aa22cf | ||
|
|
62f689824c | ||
|
|
07ac640ac5 | ||
|
|
a2b389523d | ||
|
|
17d4203bbb | ||
|
|
ee121cff21 | ||
|
|
cf37b17869 | ||
|
|
ee656fe088 | ||
|
|
f58425a305 | ||
|
|
01c7709e0d | ||
|
|
cb0214ec1d | ||
|
|
4762ba0943 | ||
|
|
d1635971a1 | ||
|
|
9467bd66c2 | ||
|
|
59107324ce | ||
|
|
b3d7ba569b | ||
|
|
c7fe58683d | ||
|
|
28b06dae25 | ||
|
|
a90cff1cd2 | ||
|
|
82eafeaf4a | ||
|
|
541c8d339b | ||
|
|
6bf6f85584 | ||
|
|
c7272fd6a5 | ||
|
|
466da7cbda | ||
|
|
2fc65094b7 | ||
|
|
d7ff77072a | ||
|
|
78660022bf | ||
|
|
aaf767f8d3 | ||
|
|
79014ed8c8 | ||
|
|
e774006052 | ||
|
|
1d6d5f7f5f | ||
|
|
433a7b8a42 | ||
|
|
e55277f26d | ||
|
|
c2b4231cc3 | ||
|
|
c150b4e358 | ||
|
|
3f0475efb7 | ||
|
|
2e376d067f | ||
|
|
8e893dfc20 | ||
|
|
756d0f29af | ||
|
|
e2f7befbfb | ||
|
|
f14742b3f3 | ||
|
|
3aae316456 | ||
|
|
063e9da65d | ||
|
|
bf1102c3f1 | ||
|
|
bbd2d42dbb | ||
|
|
d84120f499 | ||
|
|
9480f10ccd | ||
|
|
1e4d26f526 | ||
|
|
e1c1832f84 | ||
|
|
9a73380f29 | ||
|
|
2e718cf865 | ||
|
|
4000a84624 | ||
|
|
99d172f0d1 | ||
|
|
3f5c601620 | ||
|
|
8478e0bef7 | ||
|
|
0327fdc88a | ||
|
|
88b3d1e9bb | ||
|
|
204b790012 | ||
|
|
35699f6fdc | ||
|
|
2e7fd031d4 | ||
|
|
32296f5943 | ||
|
|
4fc17edd14 | ||
|
|
38efe4ec5f | ||
|
|
05a61897f2 | ||
|
|
5a7c200911 | ||
|
|
72616db81f | ||
|
|
4ab9f393fc | ||
|
|
9744f4c009 | ||
|
|
d21fa84335 | ||
|
|
d5d95da3b6 | ||
|
|
c0c089ffd5 | ||
|
|
376c42893b | ||
|
|
81d8019207 | ||
|
|
8ce88e11fd | ||
|
|
f8b832e6c3 | ||
|
|
a00a3df10c | ||
|
|
cfbb9f703b | ||
|
|
6280146c81 | ||
|
|
e8f7108e6e | ||
|
|
023ca1be32 | ||
|
|
2e3c7fa158 | ||
|
|
4da3be864f | ||
|
|
608dbded06 | ||
|
|
791d1f9552 | ||
|
|
e5e97b5c4e | ||
|
|
8219f8d55b | ||
|
|
026be3c0f1 | ||
|
|
0d4f19043e | ||
|
|
38b47e484c | ||
|
|
7943c73d3f | ||
|
|
25ce245c03 | ||
|
|
7de7a7990a | ||
|
|
f79cb9587f | ||
|
|
c27d5b11d4 | ||
|
|
5d54e6e847 | ||
|
|
631d50d038 | ||
|
|
269006bf04 | ||
|
|
e2ed89f700 | ||
|
|
badb957888 | ||
|
|
a1bba36d51 | ||
|
|
c019cd51da | ||
|
|
5406d8a366 | ||
|
|
098c255a2d | ||
|
|
2c5d64ac9e | ||
|
|
1764e1c3f6 | ||
|
|
c560ffd811 | ||
|
|
7d1b292ad0 | ||
|
|
f07581f504 | ||
|
|
d676e5e781 | ||
|
|
903d29b6f7 | ||
|
|
79a955b1a9 | ||
|
|
1bd9de9047 | ||
|
|
68f8df9594 | ||
|
|
c69cdd9f4a | ||
|
|
5842fc2a64 | ||
|
|
f0307fdd62 | ||
|
|
74013d7da7 | ||
|
|
6afbffce28 | ||
|
|
07e76907f6 | ||
|
|
4bf17a94b9 | ||
|
|
4ac8fdf6df | ||
|
|
087a6356b9 | ||
|
|
26c201f7f7 | ||
|
|
a3c5550217 | ||
|
|
798af57853 | ||
|
|
f50302b2ba | ||
|
|
b09cf6c6b5 | ||
|
|
334a775cb9 | ||
|
|
f1b6f38ea8 | ||
|
|
f6f8332b7f | ||
|
|
74351567ab | ||
|
|
51d4c9b4ab | ||
|
|
4601ca3735 | ||
|
|
1a2a49840b | ||
|
|
42fa9e4965 | ||
|
|
8046f09562 | ||
|
|
b4ed599481 | ||
|
|
e925d9d4a5 | ||
|
|
da3624644a | ||
|
|
0580328ad9 | ||
|
|
291da8cd12 | ||
|
|
8a3cbf2fdd | ||
|
|
ef08d0fa39 | ||
|
|
75d625b697 | ||
|
|
88ba8b78fc | ||
|
|
3f19aa589c | ||
|
|
630c18e0d3 | ||
|
|
1860bab24b | ||
|
|
e31839a370 | ||
|
|
0f1aed4477 | ||
|
|
ddb338d834 | ||
|
|
354c72bbf4 | ||
|
|
d63a1f5f80 | ||
|
|
645206735e | ||
|
|
95a04cadea | ||
|
|
dd458a2b33 | ||
|
|
5f3d4d285b | ||
|
|
10c3cc27e8 | ||
|
|
a07bfe7dfa | ||
|
|
451cb9d5ca | ||
|
|
fd02bef0a3 | ||
|
|
02bd52728e | ||
|
|
c038f13d44 | ||
|
|
8102ad4edc | ||
|
|
13485ca5e7 | ||
|
|
b2b332e8b2 | ||
|
|
a42bac6fcc | ||
|
|
98a6c6efbe | ||
|
|
55a90a2ee0 | ||
|
|
6e835fd8fc | ||
|
|
ee80349ea0 | ||
|
|
52b54bbad9 | ||
|
|
66d9c5637b | ||
|
|
244b797a19 | ||
|
|
95176d7e0c | ||
|
|
5a7bab972c | ||
|
|
b69259534d | ||
|
|
44422853be | ||
|
|
cc731c7b19 | ||
|
|
5d156b8241 | ||
|
|
05715489c4 | ||
|
|
661d256b85 | ||
|
|
dd30c99bc2 | ||
|
|
16ebcb2455 | ||
|
|
305838582b | ||
|
|
a260fb2774 | ||
|
|
f83cae35f2 | ||
|
|
0fed17f1c8 | ||
|
|
da44db92e9 | ||
|
|
4a499c946e | ||
|
|
99c28a94a4 | ||
|
|
f7ac4e98af | ||
|
|
394ec8d215 | ||
|
|
7845381331 | ||
|
|
7464079a09 | ||
|
|
8a574c9ed9 | ||
|
|
681d250fdc | ||
|
|
2022ffa0e5 | ||
|
|
dfcc07e512 | ||
|
|
2fa238af7c | ||
|
|
63bbd1e3ed | ||
|
|
81b87dd108 | ||
|
|
f473bb75a8 | ||
|
|
539bc2afcb | ||
|
|
9fd532246a | ||
|
|
11e926ab2f | ||
|
|
168c5aed51 | ||
|
|
d02c56aa5f | ||
|
|
829a71cd92 | ||
|
|
34ace36a9e | ||
|
|
100cf1003d | ||
|
|
f11a7b5e9f | ||
|
|
434aa7fd46 | ||
|
|
5fb0423b72 | ||
|
|
7998520848 | ||
|
|
d05000ff21 | ||
|
|
f66e77e2c6 | ||
|
|
928c4a55ff | ||
|
|
1899eb50d0 | ||
|
|
b5ec4d4f08 | ||
|
|
61362c04fa | ||
|
|
e3140063d4 | ||
|
|
1d9ef34ec7 | ||
|
|
302735097e | ||
|
|
cc0a376aba | ||
|
|
b379dd9f27 | ||
|
|
a81e0c8008 | ||
|
|
41cbbe09b2 | ||
|
|
7714bc0521 | ||
|
|
096d2b7bff | ||
|
|
7cfdffab5f | ||
|
|
f43953d225 | ||
|
|
eea1b48cbb | ||
|
|
a063cf300b | ||
|
|
b73903519c | ||
|
|
304be4a5a1 | ||
|
|
c4cbe211a3 | ||
|
|
01230aa766 | ||
|
|
5f90d03718 | ||
|
|
e2ebc9769f | ||
|
|
2e6e3b0c74 | ||
|
|
24584a4d27 | ||
|
|
8e1205f7ab | ||
|
|
e92b0fa629 | ||
|
|
bcc0751a40 | ||
|
|
f1169e56fc | ||
|
|
98e737fc27 | ||
|
|
ff34594284 | ||
|
|
9e953b1da3 | ||
|
|
fbda21f4f4 | ||
|
|
d23b4d4cbd | ||
|
|
f3acff81f3 | ||
|
|
fa0a9fe8f9 | ||
|
|
284bae9d5f | ||
|
|
3ee1af0dc6 | ||
|
|
0c5f03235e | ||
|
|
5e8aa4da5e | ||
|
|
abfec62219 | ||
|
|
0c05855131 | ||
|
|
1150772d06 | ||
|
|
669fee84d5 | ||
|
|
aa151da5fe | ||
|
|
4bf677d555 | ||
|
|
f5e20521e0 | ||
|
|
8f6a317fef | ||
|
|
d339ffdcb5 | ||
|
|
e84ffc6003 | ||
|
|
bb52a25c8a | ||
|
|
4b7b709473 | ||
|
|
2ef2eed382 | ||
|
|
f6716092af | ||
|
|
0076b19893 | ||
|
|
2642a36161 | ||
|
|
1981b249f1 | ||
|
|
a3ebb31e3c | ||
|
|
9b9a7d6c98 | ||
|
|
57a13ae355 | ||
|
|
890fb3f0e8 | ||
|
|
e236054668 | ||
|
|
3013c8b691 | ||
|
|
88c81d33ed | ||
|
|
988041ba6d | ||
|
|
550e2fc7c3 | ||
|
|
43b53ac173 | ||
|
|
08804842fa | ||
|
|
284814153d | ||
|
|
8847ba5d0b | ||
|
|
48528efff3 | ||
|
|
8d856a7c7b | ||
|
|
38a090c218 | ||
|
|
deb7052f41 | ||
|
|
9650473298 | ||
|
|
95201840d2 | ||
|
|
6ba595344a | ||
|
|
9995950c70 | ||
|
|
b6a496f18c | ||
|
|
0853a50f4d | ||
|
|
a1b82c9664 | ||
|
|
ad8604c223 | ||
|
|
d80ee966dc | ||
|
|
1530ef354f | ||
|
|
cdac2245b5 | ||
|
|
8dba4c33dd | ||
|
|
b81c9805dc | ||
|
|
ba55963104 | ||
|
|
8c91605541 | ||
|
|
4c0aef09b8 | ||
|
|
02fb757c21 | ||
|
|
674a54512c | ||
|
|
21ba5d0806 | ||
|
|
0ba57a5936 | ||
|
|
3269bc98d4 | ||
|
|
7f3382f7eb | ||
|
|
65e5ff63f0 | ||
|
|
d287c22b69 | ||
|
|
e8be93a8eb | ||
|
|
e58050bac0 | ||
|
|
36a0945c95 | ||
|
|
396441e921 | ||
|
|
972e4cdaeb | ||
|
|
99f55f6a81 | ||
|
|
285112fe29 | ||
|
|
f29bea5921 | ||
|
|
39cdbc42aa | ||
|
|
27b62ce833 | ||
|
|
499e7c4e16 | ||
|
|
0f75f975c8 | ||
|
|
7427445fe9 | ||
|
|
09a5baf523 | ||
|
|
33c08c8545 | ||
|
|
fd002db802 | ||
|
|
cb3e4b8e49 | ||
|
|
5917f8d8f9 | ||
|
|
70e6d0f2e3 | ||
|
|
31a763c46d | ||
|
|
ae6c9c2e97 | ||
|
|
f6a0dd33d1 | ||
|
|
b77c104aa6 | ||
|
|
10ca7f56ab | ||
|
|
6b2dc08ffb | ||
|
|
03971ace43 | ||
|
|
07aee798b0 | ||
|
|
6cf668f749 | ||
|
|
2ec4371c95 | ||
|
|
fc4d28f365 | ||
|
|
5290dfb9c2 | ||
|
|
05611c283d | ||
|
|
934644232a | ||
|
|
641bccc79b | ||
|
|
ac94bf530c | ||
|
|
e87cfe7319 | ||
|
|
260cb4c72d | ||
|
|
85bddaa43d | ||
|
|
a864dc9505 | ||
|
|
44e8ae2577 | ||
|
|
ac8f72fdd1 | ||
|
|
e063471c66 | ||
|
|
645debe7a4 | ||
|
|
4bfb849310 | ||
|
|
d524bd5fc3 | ||
|
|
a8416d674b | ||
|
|
725529f43f | ||
|
|
e480443c8c | ||
|
|
db386baf81 | ||
|
|
795a63f1c9 | ||
|
|
d4f9993b8d | ||
|
|
bb6fc6525c | ||
|
|
bc1edb9c8a | ||
|
|
64be0a1072 | ||
|
|
e3d6ffd30e | ||
|
|
8d36b21237 | ||
|
|
6a7d6e6071 |
+96
@@ -0,0 +1,96 @@
|
|||||||
|
tools:
|
||||||
|
- name: binny
|
||||||
|
version:
|
||||||
|
want: v0.6.3
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: anchore/binny
|
||||||
|
|
||||||
|
- name: quill
|
||||||
|
version:
|
||||||
|
want: v0.4.1
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: anchore/quill
|
||||||
|
|
||||||
|
- name: golangci-lint
|
||||||
|
version:
|
||||||
|
want: v1.55.2
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: golangci/golangci-lint
|
||||||
|
|
||||||
|
- name: glow
|
||||||
|
version:
|
||||||
|
want: v1.5.1
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: charmbracelet/glow
|
||||||
|
|
||||||
|
- name: cosign
|
||||||
|
version:
|
||||||
|
want: v2.2.2
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: sigstore/cosign
|
||||||
|
|
||||||
|
- name: yajsv
|
||||||
|
version:
|
||||||
|
want: v1.4.1
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: neilpa/yajsv
|
||||||
|
|
||||||
|
- name: goreleaser
|
||||||
|
version:
|
||||||
|
want: v1.23.0
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: goreleaser/goreleaser
|
||||||
|
|
||||||
|
- name: gosimports
|
||||||
|
version:
|
||||||
|
want: v0.3.8
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: rinchsan/gosimports
|
||||||
|
|
||||||
|
- name: chronicle
|
||||||
|
version:
|
||||||
|
want: v0.8.0
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: anchore/chronicle
|
||||||
|
|
||||||
|
- name: bouncer
|
||||||
|
version:
|
||||||
|
want: v0.4.0
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: wagoodman/go-bouncer
|
||||||
|
|
||||||
|
- name: benchstat
|
||||||
|
version:
|
||||||
|
want: latest
|
||||||
|
method: go-proxy
|
||||||
|
with:
|
||||||
|
module: golang.org/x/perf
|
||||||
|
allow-unresolved-version: true
|
||||||
|
method: go-install
|
||||||
|
with:
|
||||||
|
entrypoint: cmd/benchstat
|
||||||
|
module: golang.org/x/perf
|
||||||
|
|
||||||
|
- name: task
|
||||||
|
version:
|
||||||
|
want: v3.33.1
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: go-task/task
|
||||||
|
|
||||||
|
- name: gh
|
||||||
|
version:
|
||||||
|
want: v2.42.0
|
||||||
|
method: github-release
|
||||||
|
with:
|
||||||
|
repo: cli/cli
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
enforce-v0: true # don't make breaking-change label bump major version before 1.0.
|
||||||
|
title: ""
|
||||||
|
|
||||||
|
github:
|
||||||
|
host: github.com
|
||||||
|
include-issue-pr-authors: true
|
||||||
|
include-issue-prs: true
|
||||||
|
include-issues-not-planned: false
|
||||||
|
include-prs: true
|
||||||
|
include-issues: true
|
||||||
|
include-unlabeled-issues: true
|
||||||
|
include-unlabeled-prs: true
|
||||||
|
issues-require-linked-prs: false
|
||||||
|
consider-pr-merge-commits: true
|
||||||
|
|
||||||
|
exclude-labels:
|
||||||
|
- duplicate
|
||||||
|
- question
|
||||||
|
- invalid
|
||||||
|
- wontfix
|
||||||
|
- wont-fix
|
||||||
|
- release-ignore
|
||||||
|
- changelog-ignore
|
||||||
|
- ignore
|
||||||
|
|
||||||
|
changes:
|
||||||
|
|
||||||
|
- name: security-fixes
|
||||||
|
title: Security Fixes
|
||||||
|
semver-field: patch
|
||||||
|
labels:
|
||||||
|
- security
|
||||||
|
- vulnerability
|
||||||
|
|
||||||
|
- name: added-feature
|
||||||
|
title: Added Features
|
||||||
|
semver-field: minor
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
- feature
|
||||||
|
- minor
|
||||||
|
|
||||||
|
- name: bug-fix
|
||||||
|
title: Bug Fixes
|
||||||
|
semver-field: patch
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
- fix
|
||||||
|
- bug-fix
|
||||||
|
- patch
|
||||||
|
|
||||||
|
- name: breaking-feature
|
||||||
|
title: Breaking Changes
|
||||||
|
semver-field: major
|
||||||
|
labels:
|
||||||
|
- breaking
|
||||||
|
- backwards-incompatible
|
||||||
|
- breaking-change
|
||||||
|
- breaking-feature
|
||||||
|
- major
|
||||||
|
- detected-breaking-change
|
||||||
|
|
||||||
|
- name: removed-feature
|
||||||
|
title: Removed Features
|
||||||
|
semver-field: major
|
||||||
|
labels:
|
||||||
|
- removed
|
||||||
|
|
||||||
|
- name: deprecated-feature
|
||||||
|
title: Deprecated Features
|
||||||
|
semver-field: minor
|
||||||
|
labels:
|
||||||
|
- deprecated
|
||||||
|
|
||||||
|
- name: unknown
|
||||||
|
title: Additional Changes
|
||||||
|
semver-field: ""
|
||||||
|
labels: []
|
||||||
@@ -7,12 +7,12 @@ assignees: ''
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Please provide a set of steps on how to reproduce the issue**
|
|
||||||
|
|
||||||
**What happened**:
|
**What happened**:
|
||||||
|
|
||||||
**What you expected to happen**:
|
**What you expected to happen**:
|
||||||
|
|
||||||
|
**Steps to reproduce the issue**:
|
||||||
|
|
||||||
**Anything else we need to know?**:
|
**Anything else we need to know?**:
|
||||||
|
|
||||||
**Environment**:
|
**Environment**:
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: "Bootstrap"
|
||||||
|
|
||||||
|
description: "Bootstrap all tools and dependencies"
|
||||||
|
inputs:
|
||||||
|
go-version:
|
||||||
|
description: "Go version to install"
|
||||||
|
required: true
|
||||||
|
default: "1.21.x"
|
||||||
|
go-dependencies:
|
||||||
|
description: "Download go dependencies"
|
||||||
|
required: true
|
||||||
|
default: "true"
|
||||||
|
cache-key-prefix:
|
||||||
|
description: "Prefix all cache keys with this value"
|
||||||
|
required: true
|
||||||
|
default: "1ac8281053"
|
||||||
|
compute-fingerprints:
|
||||||
|
description: "Compute test fixture fingerprints"
|
||||||
|
required: true
|
||||||
|
default: "true"
|
||||||
|
bootstrap-apt-packages:
|
||||||
|
description: "Space delimited list of tools to install via apt"
|
||||||
|
default: "libxml2-utils"
|
||||||
|
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
# note: go mod and build is automatically cached on default with v4+
|
||||||
|
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0
|
||||||
|
if: inputs.go-version != ''
|
||||||
|
with:
|
||||||
|
go-version: ${{ inputs.go-version }}
|
||||||
|
|
||||||
|
- name: Restore tool cache
|
||||||
|
id: tool-cache
|
||||||
|
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/.tool
|
||||||
|
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('.binny.yaml') }}
|
||||||
|
|
||||||
|
- name: Install project tools
|
||||||
|
shell: bash
|
||||||
|
run: make tools
|
||||||
|
|
||||||
|
- name: Install go dependencies
|
||||||
|
if: inputs.go-dependencies == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: make ci-bootstrap-go
|
||||||
|
|
||||||
|
- name: Install apt packages
|
||||||
|
if: inputs.bootstrap-apt-packages != ''
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y ${{ inputs.bootstrap-apt-packages }}
|
||||||
|
|
||||||
|
- name: Create all cache fingerprints
|
||||||
|
if: inputs.compute-fingerprints == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: make fingerprints
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# current limit for fixture size
|
||||||
|
size=1000
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "Usage: $0 <directory>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
directory="$1"
|
||||||
|
|
||||||
|
# Remove trailing slash using parameter expansion
|
||||||
|
directory="${directory%/}"
|
||||||
|
|
||||||
|
if [ ! -d "$directory" ]; then
|
||||||
|
echo "Directory not found: $directory"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
found_large_files=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
if [ $(wc -c < "$file") -gt $size ]; then
|
||||||
|
echo "File $file is greater than ${size} bytes."
|
||||||
|
found_large_files=1
|
||||||
|
fi
|
||||||
|
done < <(find "$directory" -type f -print0)
|
||||||
|
|
||||||
|
if [ "$found_large_files" -eq 1 ]; then
|
||||||
|
echo "Script failed: Some files are greater than ${size} bytes."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "All files in $directory and its subdirectories are ${size} bytes or smaller. Check passed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
red=$(tput setaf 1)
|
||||||
|
bold=$(tput bold)
|
||||||
|
normal=$(tput sgr0)
|
||||||
|
|
||||||
|
# assert we are running in CI (or die!)
|
||||||
|
if [[ -z "$CI" ]]; then
|
||||||
|
echo "${bold}${red}This step should ONLY be run in CI. Exiting...${normal}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Executable
+36
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
|
||||||
|
class bcolors:
|
||||||
|
HEADER = '\033[95m'
|
||||||
|
OKBLUE = '\033[94m'
|
||||||
|
OKCYAN = '\033[96m'
|
||||||
|
OKGREEN = '\033[92m'
|
||||||
|
WARNING = '\033[93m'
|
||||||
|
FAIL = '\033[91m'
|
||||||
|
ENDC = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
UNDERLINE = '\033[4m'
|
||||||
|
|
||||||
|
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Usage: coverage.py [threshold] [go-coverage-report]")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
threshold = float(sys.argv[1])
|
||||||
|
report = sys.argv[2]
|
||||||
|
|
||||||
|
|
||||||
|
args = shlex.split(f"go tool cover -func {report}")
|
||||||
|
p = subprocess.run(args, capture_output=True, text=True)
|
||||||
|
|
||||||
|
percent_coverage = float(p.stdout.splitlines()[-1].split()[-1].replace("%", ""))
|
||||||
|
print(f"{bcolors.BOLD}Coverage: {percent_coverage}%{bcolors.ENDC}")
|
||||||
|
|
||||||
|
if percent_coverage < threshold:
|
||||||
|
print(f"{bcolors.BOLD}{bcolors.FAIL}Coverage below threshold of {threshold}%{bcolors.ENDC}")
|
||||||
|
sys.exit(1)
|
||||||
@@ -4,19 +4,18 @@ set -eu
|
|||||||
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
|
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
|
||||||
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
|
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
|
||||||
|
|
||||||
trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
|
trap "cp -p ${ORIGINAL_STATE_DIR}/* ./ && git update-index -q --refresh && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
|
||||||
|
|
||||||
echo "Capturing original state of files..."
|
# capturing original state of files...
|
||||||
cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
|
cp go.mod go.sum "${ORIGINAL_STATE_DIR}"
|
||||||
|
|
||||||
echo "Capturing state of go.mod and go.sum after running go mod tidy..."
|
# capturing state of go.mod and go.sum after running go mod tidy...
|
||||||
go mod tidy
|
go mod tidy
|
||||||
cp -v go.mod go.sum "${TIDY_STATE_DIR}"
|
cp go.mod go.sum "${TIDY_STATE_DIR}"
|
||||||
echo ""
|
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
|
|
||||||
# Detect difference between the git HEAD state and the go mod tidy state
|
# detect difference between the git HEAD state and the go mod tidy state
|
||||||
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
|
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
|
||||||
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
|
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
|
||||||
|
|
||||||
|
|||||||
@@ -1,398 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
# Code generated by godownloader on 2019-12-25T12:47:14Z. DO NOT EDIT.
|
|
||||||
#
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
this=$1
|
|
||||||
cat <<EOF
|
|
||||||
$this: download go binaries for goreleaser/goreleaser
|
|
||||||
|
|
||||||
Usage: $this [-b] bindir [-d] [tag]
|
|
||||||
-b sets bindir or installation directory, Defaults to ./bin
|
|
||||||
-d turns on debug logging
|
|
||||||
[tag] is a tag from
|
|
||||||
https://github.com/goreleaser/goreleaser/releases
|
|
||||||
If tag is missing, then the latest will be used.
|
|
||||||
|
|
||||||
Generated by godownloader
|
|
||||||
https://github.com/goreleaser/godownloader
|
|
||||||
|
|
||||||
EOF
|
|
||||||
exit 2
|
|
||||||
}
|
|
||||||
|
|
||||||
parse_args() {
|
|
||||||
#BINDIR is ./bin unless set be ENV
|
|
||||||
# over-ridden by flag below
|
|
||||||
|
|
||||||
BINDIR=${BINDIR:-./bin}
|
|
||||||
while getopts "b:dh?x" arg; do
|
|
||||||
case "$arg" in
|
|
||||||
b) BINDIR="$OPTARG" ;;
|
|
||||||
d) log_set_priority 10 ;;
|
|
||||||
h | \?) usage "$0" ;;
|
|
||||||
x) set -x ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
shift $((OPTIND - 1))
|
|
||||||
TAG=$1
|
|
||||||
}
|
|
||||||
# this function wraps all the destructive operations
|
|
||||||
# if a curl|bash cuts off the end of the script due to
|
|
||||||
# network, either nothing will happen or will syntax error
|
|
||||||
# out preventing half-done work
|
|
||||||
execute() {
|
|
||||||
tmpdir=$(mktemp -d)
|
|
||||||
log_debug "downloading files into ${tmpdir}"
|
|
||||||
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
|
|
||||||
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
|
|
||||||
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
|
|
||||||
srcdir="${tmpdir}"
|
|
||||||
(cd "${tmpdir}" && untar "${TARBALL}")
|
|
||||||
test ! -d "${BINDIR}" && install -d "${BINDIR}"
|
|
||||||
for binexe in $BINARIES; do
|
|
||||||
if [ "$OS" = "windows" ]; then
|
|
||||||
binexe="${binexe}.exe"
|
|
||||||
fi
|
|
||||||
install "${srcdir}/${binexe}" "${BINDIR}/"
|
|
||||||
log_info "installed ${BINDIR}/${binexe}"
|
|
||||||
done
|
|
||||||
rm -rf "${tmpdir}"
|
|
||||||
}
|
|
||||||
get_binaries() {
|
|
||||||
case "$PLATFORM" in
|
|
||||||
darwin/386) BINARIES="goreleaser" ;;
|
|
||||||
darwin/amd64) BINARIES="goreleaser" ;;
|
|
||||||
darwin/arm64) BINARIES="goreleaser" ;;
|
|
||||||
darwin/armv6) BINARIES="goreleaser" ;;
|
|
||||||
linux/386) BINARIES="goreleaser" ;;
|
|
||||||
linux/amd64) BINARIES="goreleaser" ;;
|
|
||||||
linux/arm64) BINARIES="goreleaser" ;;
|
|
||||||
linux/armv6) BINARIES="goreleaser" ;;
|
|
||||||
windows/386) BINARIES="goreleaser" ;;
|
|
||||||
windows/amd64) BINARIES="goreleaser" ;;
|
|
||||||
windows/arm64) BINARIES="goreleaser" ;;
|
|
||||||
windows/armv6) BINARIES="goreleaser" ;;
|
|
||||||
*)
|
|
||||||
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
tag_to_version() {
|
|
||||||
if [ -z "${TAG}" ]; then
|
|
||||||
log_info "checking GitHub for latest tag"
|
|
||||||
else
|
|
||||||
log_info "checking GitHub for tag '${TAG}'"
|
|
||||||
fi
|
|
||||||
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
|
|
||||||
if test -z "$REALTAG"; then
|
|
||||||
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# if version starts with 'v', remove it
|
|
||||||
TAG="$REALTAG"
|
|
||||||
VERSION=${TAG#v}
|
|
||||||
}
|
|
||||||
adjust_format() {
|
|
||||||
# change format (tar.gz or zip) based on OS
|
|
||||||
case ${OS} in
|
|
||||||
windows) FORMAT=zip ;;
|
|
||||||
esac
|
|
||||||
true
|
|
||||||
}
|
|
||||||
adjust_os() {
|
|
||||||
# adjust archive name based on OS
|
|
||||||
case ${OS} in
|
|
||||||
386) OS=i386 ;;
|
|
||||||
amd64) OS=x86_64 ;;
|
|
||||||
darwin) OS=Darwin ;;
|
|
||||||
linux) OS=Linux ;;
|
|
||||||
windows) OS=Windows ;;
|
|
||||||
esac
|
|
||||||
true
|
|
||||||
}
|
|
||||||
adjust_arch() {
|
|
||||||
# adjust archive name based on ARCH
|
|
||||||
case ${ARCH} in
|
|
||||||
386) ARCH=i386 ;;
|
|
||||||
amd64) ARCH=x86_64 ;;
|
|
||||||
darwin) ARCH=Darwin ;;
|
|
||||||
linux) ARCH=Linux ;;
|
|
||||||
windows) ARCH=Windows ;;
|
|
||||||
esac
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
cat /dev/null <<EOF
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
https://github.com/client9/shlib - portable posix shell functions
|
|
||||||
Public domain - http://unlicense.org
|
|
||||||
https://github.com/client9/shlib/blob/master/LICENSE.md
|
|
||||||
but credit (and pull requests) appreciated.
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
EOF
|
|
||||||
is_command() {
|
|
||||||
command -v "$1" >/dev/null
|
|
||||||
}
|
|
||||||
echoerr() {
|
|
||||||
echo "$@" 1>&2
|
|
||||||
}
|
|
||||||
log_prefix() {
|
|
||||||
echo "$0"
|
|
||||||
}
|
|
||||||
_logp=6
|
|
||||||
log_set_priority() {
|
|
||||||
_logp="$1"
|
|
||||||
}
|
|
||||||
log_priority() {
|
|
||||||
if test -z "$1"; then
|
|
||||||
echo "$_logp"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
[ "$1" -le "$_logp" ]
|
|
||||||
}
|
|
||||||
log_tag() {
|
|
||||||
case $1 in
|
|
||||||
0) echo "emerg" ;;
|
|
||||||
1) echo "alert" ;;
|
|
||||||
2) echo "crit" ;;
|
|
||||||
3) echo "err" ;;
|
|
||||||
4) echo "warning" ;;
|
|
||||||
5) echo "notice" ;;
|
|
||||||
6) echo "info" ;;
|
|
||||||
7) echo "debug" ;;
|
|
||||||
*) echo "$1" ;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
log_debug() {
|
|
||||||
log_priority 7 || return 0
|
|
||||||
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
|
|
||||||
}
|
|
||||||
log_info() {
|
|
||||||
log_priority 6 || return 0
|
|
||||||
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
|
|
||||||
}
|
|
||||||
log_err() {
|
|
||||||
log_priority 3 || return 0
|
|
||||||
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
|
|
||||||
}
|
|
||||||
log_crit() {
|
|
||||||
log_priority 2 || return 0
|
|
||||||
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
|
|
||||||
}
|
|
||||||
uname_os() {
|
|
||||||
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
||||||
case "$os" in
|
|
||||||
cygwin_nt*) os="windows" ;;
|
|
||||||
mingw*) os="windows" ;;
|
|
||||||
msys_nt*) os="windows" ;;
|
|
||||||
esac
|
|
||||||
echo "$os"
|
|
||||||
}
|
|
||||||
uname_arch() {
|
|
||||||
arch=$(uname -m)
|
|
||||||
case $arch in
|
|
||||||
x86_64) arch="amd64" ;;
|
|
||||||
x86) arch="386" ;;
|
|
||||||
i686) arch="386" ;;
|
|
||||||
i386) arch="386" ;;
|
|
||||||
aarch64) arch="arm64" ;;
|
|
||||||
armv5*) arch="armv5" ;;
|
|
||||||
armv6*) arch="armv6" ;;
|
|
||||||
armv7*) arch="armv7" ;;
|
|
||||||
esac
|
|
||||||
echo ${arch}
|
|
||||||
}
|
|
||||||
uname_os_check() {
|
|
||||||
os=$(uname_os)
|
|
||||||
case "$os" in
|
|
||||||
darwin) return 0 ;;
|
|
||||||
dragonfly) return 0 ;;
|
|
||||||
freebsd) return 0 ;;
|
|
||||||
linux) return 0 ;;
|
|
||||||
android) return 0 ;;
|
|
||||||
nacl) return 0 ;;
|
|
||||||
netbsd) return 0 ;;
|
|
||||||
openbsd) return 0 ;;
|
|
||||||
plan9) return 0 ;;
|
|
||||||
solaris) return 0 ;;
|
|
||||||
windows) return 0 ;;
|
|
||||||
esac
|
|
||||||
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
uname_arch_check() {
|
|
||||||
arch=$(uname_arch)
|
|
||||||
case "$arch" in
|
|
||||||
386) return 0 ;;
|
|
||||||
amd64) return 0 ;;
|
|
||||||
arm64) return 0 ;;
|
|
||||||
armv5) return 0 ;;
|
|
||||||
armv6) return 0 ;;
|
|
||||||
armv7) return 0 ;;
|
|
||||||
ppc64) return 0 ;;
|
|
||||||
ppc64le) return 0 ;;
|
|
||||||
mips) return 0 ;;
|
|
||||||
mipsle) return 0 ;;
|
|
||||||
mips64) return 0 ;;
|
|
||||||
mips64le) return 0 ;;
|
|
||||||
s390x) return 0 ;;
|
|
||||||
amd64p32) return 0 ;;
|
|
||||||
esac
|
|
||||||
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
untar() {
|
|
||||||
tarball=$1
|
|
||||||
case "${tarball}" in
|
|
||||||
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;;
|
|
||||||
*.tar) tar --no-same-owner -xf "${tarball}" ;;
|
|
||||||
*.zip) unzip "${tarball}" ;;
|
|
||||||
*)
|
|
||||||
log_err "untar unknown archive format for ${tarball}"
|
|
||||||
return 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
http_download_curl() {
|
|
||||||
local_file=$1
|
|
||||||
source_url=$2
|
|
||||||
header=$3
|
|
||||||
if [ -z "$header" ]; then
|
|
||||||
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
|
|
||||||
else
|
|
||||||
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
|
|
||||||
fi
|
|
||||||
if [ "$code" != "200" ]; then
|
|
||||||
log_debug "http_download_curl received HTTP status $code"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
http_download_wget() {
|
|
||||||
local_file=$1
|
|
||||||
source_url=$2
|
|
||||||
header=$3
|
|
||||||
if [ -z "$header" ]; then
|
|
||||||
wget -q -O "$local_file" "$source_url"
|
|
||||||
else
|
|
||||||
wget -q --header "$header" -O "$local_file" "$source_url"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
http_download() {
|
|
||||||
log_debug "http_download $2"
|
|
||||||
if is_command curl; then
|
|
||||||
http_download_curl "$@"
|
|
||||||
return
|
|
||||||
elif is_command wget; then
|
|
||||||
http_download_wget "$@"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
log_crit "http_download unable to find wget or curl"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
http_copy() {
|
|
||||||
tmp=$(mktemp)
|
|
||||||
http_download "${tmp}" "$1" "$2" || return 1
|
|
||||||
body=$(cat "$tmp")
|
|
||||||
rm -f "${tmp}"
|
|
||||||
echo "$body"
|
|
||||||
}
|
|
||||||
github_release() {
|
|
||||||
owner_repo=$1
|
|
||||||
version=$2
|
|
||||||
test -z "$version" && version="latest"
|
|
||||||
giturl="https://github.com/${owner_repo}/releases/${version}"
|
|
||||||
json=$(http_copy "$giturl" "Accept:application/json")
|
|
||||||
test -z "$json" && return 1
|
|
||||||
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
|
|
||||||
test -z "$version" && return 1
|
|
||||||
echo "$version"
|
|
||||||
}
|
|
||||||
hash_sha256() {
|
|
||||||
TARGET=${1:-/dev/stdin}
|
|
||||||
if is_command gsha256sum; then
|
|
||||||
hash=$(gsha256sum "$TARGET") || return 1
|
|
||||||
echo "$hash" | cut -d ' ' -f 1
|
|
||||||
elif is_command sha256sum; then
|
|
||||||
hash=$(sha256sum "$TARGET") || return 1
|
|
||||||
echo "$hash" | cut -d ' ' -f 1
|
|
||||||
elif is_command shasum; then
|
|
||||||
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
|
|
||||||
echo "$hash" | cut -d ' ' -f 1
|
|
||||||
elif is_command openssl; then
|
|
||||||
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
|
|
||||||
echo "$hash" | cut -d ' ' -f a
|
|
||||||
else
|
|
||||||
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
hash_sha256_verify() {
|
|
||||||
TARGET=$1
|
|
||||||
checksums=$2
|
|
||||||
if [ -z "$checksums" ]; then
|
|
||||||
log_err "hash_sha256_verify checksum file not specified in arg2"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
BASENAME=${TARGET##*/}
|
|
||||||
want=$(grep "${BASENAME}$" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
|
|
||||||
if [ -z "$want" ]; then
|
|
||||||
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
got=$(hash_sha256 "$TARGET")
|
|
||||||
if [ "$want" != "$got" ]; then
|
|
||||||
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
cat /dev/null <<EOF
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
End of functions from https://github.com/client9/shlib
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
EOF
|
|
||||||
|
|
||||||
PROJECT_NAME="goreleaser"
|
|
||||||
OWNER=goreleaser
|
|
||||||
REPO="goreleaser"
|
|
||||||
BINARY=goreleaser
|
|
||||||
FORMAT=tar.gz
|
|
||||||
OS=$(uname_os)
|
|
||||||
ARCH=$(uname_arch)
|
|
||||||
PREFIX="$OWNER/$REPO"
|
|
||||||
|
|
||||||
# use in logging routines
|
|
||||||
log_prefix() {
|
|
||||||
echo "$PREFIX"
|
|
||||||
}
|
|
||||||
PLATFORM="${OS}/${ARCH}"
|
|
||||||
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
|
|
||||||
|
|
||||||
uname_os_check "$OS"
|
|
||||||
uname_arch_check "$ARCH"
|
|
||||||
|
|
||||||
parse_args "$@"
|
|
||||||
|
|
||||||
get_binaries
|
|
||||||
|
|
||||||
tag_to_version
|
|
||||||
|
|
||||||
adjust_format
|
|
||||||
|
|
||||||
adjust_os
|
|
||||||
|
|
||||||
adjust_arch
|
|
||||||
|
|
||||||
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
|
|
||||||
|
|
||||||
NAME=${PROJECT_NAME}_${OS}_${ARCH}
|
|
||||||
TARBALL=${NAME}.${FORMAT}
|
|
||||||
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
|
|
||||||
CHECKSUM=checksums.txt
|
|
||||||
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
|
|
||||||
|
|
||||||
execute
|
|
||||||
log_err "this script is deprecated, please do not use it anymore. check https://github.com/goreleaser/godownloader/issues/207"
|
|
||||||
Executable
+17
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then
|
||||||
|
echo " 🔴 there are uncommitted changes, please commit them before running this check"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! make generate-json-schema; then
|
||||||
|
echo "Generating json schema failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$(git status --porcelain | wc -l)" -ne "0" ]; then
|
||||||
|
echo " 🔴 there are uncommitted changes, please commit them before running this check"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
DRY_RUN = False
|
||||||
|
|
||||||
|
JSON_SCHEMA_LABEL = "json-schema"
|
||||||
|
|
||||||
|
# note: we can't use "breaking-change" as the label since that might be applied manually by a user. This is a
|
||||||
|
# distinct label that we can use to indicate that the label was applied (or removed) by automation.
|
||||||
|
BREAKING_CHANGE_LABEL = "detected-breaking-change"
|
||||||
|
|
||||||
|
|
||||||
|
def main(changed_files: str | None = None, merge_base_schema_files: str | None = None):
|
||||||
|
global DRY_RUN
|
||||||
|
|
||||||
|
pr_number = os.environ.get("GITHUB_PR_NUMBER")
|
||||||
|
comment_file_path = os.environ.get("CI_COMMENT_FILE")
|
||||||
|
|
||||||
|
if not comment_file_path:
|
||||||
|
print("CI_COMMENT_FILE not set")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not pr_number:
|
||||||
|
DRY_RUN = True
|
||||||
|
|
||||||
|
if changed_files:
|
||||||
|
DRY_RUN = True
|
||||||
|
|
||||||
|
# read lines from file... this is useful for local testing
|
||||||
|
with open(changed_files) as f:
|
||||||
|
pr_changed_files = f.read().splitlines()
|
||||||
|
|
||||||
|
with open(merge_base_schema_files) as f:
|
||||||
|
og_json_schema_files = sort_json_schema_files(f.read().splitlines())
|
||||||
|
|
||||||
|
else:
|
||||||
|
if not is_ci():
|
||||||
|
print("Not in CI")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not pr_number:
|
||||||
|
print("Not a PR")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
pr_changed_files = get_pr_changed_files(pr_number)
|
||||||
|
# since we are running this in the context of the pull_request_target, the checkout is the merge base..
|
||||||
|
# that is the main branch of the original repo, NOT the branch in the forked repo (or branch in the target
|
||||||
|
# repo for non-forked PRs). This means we just need to list the current checkedout files to get a sense of
|
||||||
|
# the changes before a merge.
|
||||||
|
og_json_schema_files = list_json_schema_files()
|
||||||
|
|
||||||
|
pr_json_schema_files = filter_to_schema_files(pr_changed_files)
|
||||||
|
|
||||||
|
# print("schema files in pr: ", summarize_schema_files(pr_json_schema_files))
|
||||||
|
# print("og schema files: ", summarize_schema_files(og_json_schema_files))
|
||||||
|
|
||||||
|
if not og_json_schema_files:
|
||||||
|
print("No schema files found in merge base")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# pr_json_schema_files = set of PR files are added, removed, and changed files
|
||||||
|
new_schema_files = set(pr_json_schema_files) - set(og_json_schema_files)
|
||||||
|
removed_or_modified_schema_files = set(pr_json_schema_files) - set(new_schema_files)
|
||||||
|
|
||||||
|
print("new schemas: ", summarize_schema_files(new_schema_files))
|
||||||
|
print("removed or modified schemas:", summarize_schema_files(removed_or_modified_schema_files))
|
||||||
|
|
||||||
|
# if there is a new or modified schema, we should add the "json-schema" label to the PR...
|
||||||
|
if new_schema_files or removed_or_modified_schema_files:
|
||||||
|
print("\nAdding json-schema label...")
|
||||||
|
add_label(pr_number, JSON_SCHEMA_LABEL)
|
||||||
|
|
||||||
|
else:
|
||||||
|
remove_label(pr_number, JSON_SCHEMA_LABEL)
|
||||||
|
|
||||||
|
# new schema files should be scrutinized, comparing the latest and added versions to see if it's a breaking
|
||||||
|
# change (major version bump). Warn about it on the PR via adding a breaking-change label...
|
||||||
|
if is_breaking_change(new_schema_files, og_json_schema_files[-1]):
|
||||||
|
print("\nBreaking change detected...")
|
||||||
|
add_label(pr_number, BREAKING_CHANGE_LABEL)
|
||||||
|
else:
|
||||||
|
remove_label(pr_number, BREAKING_CHANGE_LABEL)
|
||||||
|
|
||||||
|
# modifying an existing schema could be a breaking change, we should warn about it on the PR via a comment...
|
||||||
|
# removing schema files should never be allowed, we should warn about it on the PR via a comment...
|
||||||
|
if removed_or_modified_schema_files:
|
||||||
|
print("\nRemoved or modified schema detected...")
|
||||||
|
schemas = sort_json_schema_files(list(removed_or_modified_schema_files))
|
||||||
|
schemas_str = "\n".join([f" - {schema}" for schema in schemas])
|
||||||
|
add_comment(comment_file_path, f"Detected modification or removal of existing json schemas:\n{schemas_str}", warning=True)
|
||||||
|
|
||||||
|
|
||||||
|
def add_comment(comment_file_path: str, comment: str, warning: bool = False, important: bool = False):
|
||||||
|
if warning or important:
|
||||||
|
comment_lines = comment.splitlines()
|
||||||
|
comment = "\n".join([f"> {line}" for line in comment_lines])
|
||||||
|
|
||||||
|
if warning:
|
||||||
|
comment = f"> [!WARNING]\n{comment}"
|
||||||
|
elif important:
|
||||||
|
comment = f"> [!IMPORTANT]\n{comment}"
|
||||||
|
|
||||||
|
# create any parent directories if they don't exist
|
||||||
|
os.makedirs(os.path.dirname(comment_file_path), exist_ok=True)
|
||||||
|
|
||||||
|
with open(comment_file_path, "w") as f:
|
||||||
|
f.write(comment)
|
||||||
|
|
||||||
|
print(f"Comment file contents: {comment_file_path}")
|
||||||
|
print(comment)
|
||||||
|
|
||||||
|
|
||||||
|
def add_label(pr_number: str, label: str):
|
||||||
|
# run "gh pr edit --add-label <label>"
|
||||||
|
result = run(f"gh pr edit {pr_number} --add-label {label}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Unable to add {label!r} label to PR with")
|
||||||
|
print(str(result.stderr))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_label(pr_number: str, label: str):
|
||||||
|
# run "gh pr edit --remove-label <label>"
|
||||||
|
result = run(f"gh pr edit {pr_number} --remove-label {label}", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print(f"Unable to label PR with {label!r}")
|
||||||
|
print(str(result.stderr))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def major_version(semver: str) -> int:
|
||||||
|
return int(semver.split(".")[0])
|
||||||
|
|
||||||
|
|
||||||
|
def is_breaking_change(new_schema_files: set[str], latest_schema_file: str) -> bool:
|
||||||
|
latest_major_version = major_version(get_semver(latest_schema_file))
|
||||||
|
for file in new_schema_files:
|
||||||
|
change_major_version = major_version(get_semver(file))
|
||||||
|
if change_major_version > latest_major_version:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def summarize_schema_files(files: list[str]) -> list[str]:
|
||||||
|
return [get_semver(file) for file in files]
|
||||||
|
|
||||||
|
|
||||||
|
def is_ci() -> bool:
|
||||||
|
return "CI" in os.environ
|
||||||
|
|
||||||
|
|
||||||
|
def get_pr_changed_files(pr_number: str) -> list[str]:
|
||||||
|
result = run(f"gh pr view {pr_number} --json files --jq '.files.[].path'", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("Unable to get list of changed files in PR")
|
||||||
|
print(str(result.stderr))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
list_of_files = result.stdout.splitlines()
|
||||||
|
return list_of_files
|
||||||
|
|
||||||
|
|
||||||
|
def filter_to_schema_files(list_of_files: list[str]) -> list[str]:
|
||||||
|
# get files matching "schema/json/schema-*.json"
|
||||||
|
files = []
|
||||||
|
for file in list_of_files:
|
||||||
|
if re.match(r"^schema/json/schema-\d+\.\d+\.\d+\.json$", file):
|
||||||
|
files.append(file)
|
||||||
|
return sort_json_schema_files(files)
|
||||||
|
|
||||||
|
|
||||||
|
def list_json_schema_files() -> list[str]:
|
||||||
|
# list files in "schema/json" directory matching the pattern of "schema-*.json"
|
||||||
|
return sort_json_schema_files(list(glob.glob("schema/json/schema-*.json")))
|
||||||
|
|
||||||
|
|
||||||
|
def run(command: str, **kwargs) -> subprocess.CompletedProcess:
|
||||||
|
if DRY_RUN:
|
||||||
|
print(f"[DRY RUN] {command}")
|
||||||
|
return subprocess.CompletedProcess(args=[command], returncode=0)
|
||||||
|
print(f"[RUN] {command}")
|
||||||
|
return subprocess.run(command, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_semver(input_file: str) -> str:
|
||||||
|
return input_file.split("-")[1].split(".json")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def sort_json_schema_files(files: list[str]) -> list[str]:
|
||||||
|
# sort files by schema version, where the input looks like "schema/json/schema-1.12.1.json"
|
||||||
|
# we should sort by the semantic version embedded within the basename, not the string
|
||||||
|
# so that "schema/json/schema-1.2.1.json" comes before "schema/json/schema-1.12.1.json".
|
||||||
|
versions = [get_semver(file) for file in files if file]
|
||||||
|
|
||||||
|
versions = sorted(versions, key=lambda s: [int(u) for u in s.split('.')])
|
||||||
|
|
||||||
|
return [f"schema/json/schema-{version}.json" for version in versions]
|
||||||
|
|
||||||
|
|
||||||
|
# allow for test files that have line-by-line list of files:
|
||||||
|
|
||||||
|
# .binny.yaml
|
||||||
|
# .github/actions/bootstrap/action.yaml
|
||||||
|
# .github/scripts/goreleaser-install.sh
|
||||||
|
# .github/workflows/release.yaml
|
||||||
|
# .github/workflows/update-bootstrap-tools.yml
|
||||||
|
# .github/workflows/update-cpe-dictionary-index.yml
|
||||||
|
# .github/workflows/update-stereoscope-release.yml
|
||||||
|
# .github/workflows/validations.yaml
|
||||||
|
# .gitignore
|
||||||
|
# .goreleaser.yaml
|
||||||
|
# Makefile
|
||||||
|
# Taskfile.yaml
|
||||||
|
# schema/cyclonedx/Makefile
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# these are variables for a single file name that contains a list of files (line separated)
|
||||||
|
changed_files = None
|
||||||
|
merge_base_schema_files = None
|
||||||
|
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
changed_files = sys.argv[1]
|
||||||
|
merge_base_schema_files = sys.argv[2]
|
||||||
|
|
||||||
|
main(changed_files, merge_base_schema_files)
|
||||||
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import labeler
|
||||||
|
|
||||||
|
class Labeler(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_major_version(self):
|
||||||
|
self.assertEqual(labeler.major_version("1.2.3"), 1)
|
||||||
|
self.assertEqual(labeler.major_version("2.0.0"), 2)
|
||||||
|
|
||||||
|
def test_is_breaking_change(self):
|
||||||
|
new_schema_files = ["schema/json/schema-2.0.0.json"]
|
||||||
|
latest_schema_file = "schema/json/schema-1.2.0.json"
|
||||||
|
self.assertTrue(labeler.is_breaking_change(new_schema_files, latest_schema_file))
|
||||||
|
|
||||||
|
new_schema_files = ["schema/json/schema-1.3.0.json"]
|
||||||
|
latest_schema_file = "schema/json/schema-1.2.0.json"
|
||||||
|
self.assertFalse(labeler.is_breaking_change(new_schema_files, latest_schema_file))
|
||||||
|
|
||||||
|
def test_summarize_schema_files(self):
|
||||||
|
files = ["schema/json/schema-1.0.0.json", "schema/json/schema-2.0.0.json"]
|
||||||
|
expected = ["1.0.0", "2.0.0"]
|
||||||
|
self.assertEqual(labeler.summarize_schema_files(files), expected)
|
||||||
|
|
||||||
|
def test_is_ci(self):
|
||||||
|
# Mock os.environ to simulate CI environment
|
||||||
|
with patch.dict("os.environ", {"CI": "true"}):
|
||||||
|
self.assertTrue(labeler.is_ci())
|
||||||
|
|
||||||
|
def test_get_pr_changed_files(self):
|
||||||
|
expected_command = "gh pr view 123 --json files --jq '.files.[].path'"
|
||||||
|
expected_output = "file1.json\nfile2.json\n"
|
||||||
|
|
||||||
|
subprocess.CompletedProcess.returncode = 0
|
||||||
|
subprocess.CompletedProcess.stdout = expected_output
|
||||||
|
with patch("labeler.run", return_value=subprocess.CompletedProcess) as mock_run:
|
||||||
|
result = labeler.get_pr_changed_files("123")
|
||||||
|
mock_run.assert_called_with(expected_command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
|
self.assertEqual(result, ["file1.json", "file2.json"])
|
||||||
|
|
||||||
|
def test_filter_to_schema_files(self):
|
||||||
|
input_files = ["schema/json/schema-1.0.0.json", "not_schema.txt", "schema/json/schema-2.0.0.json"]
|
||||||
|
expected_files = ["schema/json/schema-1.0.0.json", "schema/json/schema-2.0.0.json"]
|
||||||
|
self.assertEqual(labeler.filter_to_schema_files(input_files), expected_files)
|
||||||
|
|
||||||
|
# we should be strict about what files are allowed to be processed
|
||||||
|
input_files = ["schema/json/schema-1.0.0extracontent.json", "schema/json/schema-1.0.0.md", "schema/json/schema-1.0.0.json.extracontent"]
|
||||||
|
expected_files = []
|
||||||
|
self.assertEqual(labeler.filter_to_schema_files(input_files), expected_files)
|
||||||
|
|
||||||
|
def test_get_semver(self):
|
||||||
|
input_file = "schema/json/schema-1.0.0.json"
|
||||||
|
expected_semver = "1.0.0"
|
||||||
|
self.assertEqual(labeler.get_semver(input_file), expected_semver)
|
||||||
|
|
||||||
|
def test_sort_json_schema_files(self):
|
||||||
|
files = ["schema/json/schema-1.12.1.json", "schema/json/schema-1.2.1.json"]
|
||||||
|
expected_sorted_files = ["schema/json/schema-1.2.1.json", "schema/json/schema-1.12.1.json"]
|
||||||
|
self.assertEqual(labeler.sort_json_schema_files(files), expected_sorted_files)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Executable
+50
@@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
bold=$(tput bold)
|
||||||
|
normal=$(tput sgr0)
|
||||||
|
|
||||||
|
if ! [ -x "$(command -v gh)" ]; then
|
||||||
|
echo "The GitHub CLI could not be found. To continue follow the instructions at https://github.com/cli/cli#installation"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh auth status
|
||||||
|
|
||||||
|
# we need all of the git state to determine the next version. Since tagging is done by
|
||||||
|
# the release pipeline it is possible to not have all of the tags from previous releases.
|
||||||
|
git fetch --tags
|
||||||
|
|
||||||
|
# populates the CHANGELOG.md and VERSION files
|
||||||
|
echo "${bold}Generating changelog...${normal}"
|
||||||
|
make changelog 2> /dev/null
|
||||||
|
|
||||||
|
NEXT_VERSION=$(cat VERSION)
|
||||||
|
|
||||||
|
if [[ "$NEXT_VERSION" == "" || "${NEXT_VERSION}" == "(Unreleased)" ]]; then
|
||||||
|
echo "Could not determine the next version to release. Exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
read -p "${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] " yn
|
||||||
|
case $yn in
|
||||||
|
[Yy]* ) echo; break;;
|
||||||
|
[Nn]* ) echo; echo "Cancelling release..."; exit;;
|
||||||
|
* ) echo "Please answer yes or no.";;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "${bold}Kicking off release for ${NEXT_VERSION}${normal}..."
|
||||||
|
echo
|
||||||
|
gh workflow run release.yaml -f version=${NEXT_VERSION}
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "${bold}Waiting for release to start...${normal}"
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
set +e
|
||||||
|
|
||||||
|
echo "${bold}Head to the release workflow to monitor the release:${normal} $(gh run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')"
|
||||||
|
id=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId')
|
||||||
|
gh run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" gh run view $id --log-failed)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
name: "Benchmark testing"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
Benchmark-Test:
|
||||||
|
name: "Benchmark tests"
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
|
||||||
|
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
|
||||||
|
# the job by event.
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
|
||||||
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
|
|
||||||
|
- name: Restore base benchmark result
|
||||||
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
|
with:
|
||||||
|
path: test/results/benchmark-main.txt
|
||||||
|
# use base sha for PR or new commit hash for main push in benchmark result key
|
||||||
|
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
|
||||||
|
|
||||||
|
- name: Run benchmark tests
|
||||||
|
id: benchmark
|
||||||
|
run: |
|
||||||
|
REF_NAME=${GITHUB_REF##*/} make benchmark
|
||||||
|
OUTPUT=$(make show-benchstat)
|
||||||
|
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
|
||||||
|
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
|
||||||
|
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
|
||||||
|
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4.1.0
|
||||||
|
with:
|
||||||
|
name: benchmark-test-results
|
||||||
|
path: test/results/**/*
|
||||||
|
|
||||||
|
- name: Update PR benchmark results comment
|
||||||
|
uses: marocchino/sticky-pull-request-comment@efaaab3fd41a9c3de579aba759d2552635e590fd #v2.8.0
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
header: benchmark
|
||||||
|
message: |
|
||||||
|
### Benchmark Test Results
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Benchmark results from the latest changes vs base branch</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
${{ steps.benchmark.outputs.result }}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
@@ -14,11 +14,17 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 3'
|
- cron: '0 0 * * 3'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
security-events: write
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -30,11 +36,16 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 #v5.0.0
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 #v3.23.0
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -45,7 +56,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 #v3.23.0
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
@@ -59,4 +70,4 @@ jobs:
|
|||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 #v3.23.0
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: "Detect schema changes"
|
||||||
|
|
||||||
|
on:
|
||||||
|
# IMPORTANT! This workflow is triggered by the `pull_request_target` event
|
||||||
|
# which means that forked PRs will run with access secrets from the repo
|
||||||
|
# it's forked from (the "target" repo).
|
||||||
|
#
|
||||||
|
# For this reason we only NEVER checkout the code from the pull request
|
||||||
|
# (e.g. "ref: ${{ github.event.pull_request.head.sha }}") to prevent
|
||||||
|
# accidentally running potentially untrusted code.
|
||||||
|
#
|
||||||
|
# By default the checkout will be:
|
||||||
|
# - GITHUB_SHA: Last commit on the PR base branch
|
||||||
|
# - GITHUB_REF: PR base branch
|
||||||
|
#
|
||||||
|
# ...unlike a typical PR where:
|
||||||
|
# - GITHUB_SHA: Last merge commit on the GITHUB_REF branch
|
||||||
|
# - GITHUB_REF: PR merge branch refs/pull/:prNumber/merge
|
||||||
|
pull_request_target:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# note: this is used within hashFiles() so must be within the GITHUB_WORKSPACE path (or will silently fail)
|
||||||
|
CI_COMMENT_FILE: .tmp/labeler-comment.txt
|
||||||
|
# needs to be any string to uniquely identify the comment on a PR across multiple runs
|
||||||
|
COMMENT_HEADER: "label-commentary"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label:
|
||||||
|
name: "Label changes"
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
|
||||||
|
- run: python .github/scripts/labeler.py
|
||||||
|
env:
|
||||||
|
# note: this token has write access to the repo
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
GITHUB_PR_NUMBER: ${{ github.event.number }}
|
||||||
|
|
||||||
|
- name: Delete existing comment
|
||||||
|
if: ${{ hashFiles( env.CI_COMMENT_FILE ) == '' }}
|
||||||
|
uses: marocchino/sticky-pull-request-comment@efaaab3fd41a9c3de579aba759d2552635e590fd #v2.8.0
|
||||||
|
with:
|
||||||
|
header: ${{ env.COMMENT_HEADER }}
|
||||||
|
hide: true
|
||||||
|
hide_classify: "OUTDATED"
|
||||||
|
|
||||||
|
- name: Add comment
|
||||||
|
if: ${{ hashFiles( env.CI_COMMENT_FILE ) != '' }}
|
||||||
|
uses: marocchino/sticky-pull-request-comment@efaaab3fd41a9c3de579aba759d2552635e590fd #v2.8.0
|
||||||
|
with:
|
||||||
|
header: ${{ env.COMMENT_HEADER }}
|
||||||
|
path: ${{ env.CI_COMMENT_FILE }}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
name: Add to OSS board
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- transferred
|
||||||
|
- labeled
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run:
|
||||||
|
uses: "anchore/workflows/.github/workflows/oss-project-board-add.yaml@main"
|
||||||
|
secrets:
|
||||||
|
token: ${{ secrets.OSS_PROJECT_GH_TOKEN }}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: "Release Homebrew"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- released
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
homebrew:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
with:
|
||||||
|
# we need to be able to get the commit for a tag
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get release info
|
||||||
|
id: release_info
|
||||||
|
run: |
|
||||||
|
echo -n "tag_name=$(gh release view --json tagName --jq '.tagName')" | tee -a $GITHUB_OUTPUT
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get commit info
|
||||||
|
id: git_info
|
||||||
|
run: |
|
||||||
|
git checkout ${{ steps.release_info.outputs.tag_name }}
|
||||||
|
echo -n "commit=$(git rev-parse HEAD)" | tee -a $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Update Homebrew formula
|
||||||
|
uses: dawidd6/action-homebrew-bump-formula@75ed025ff3ad1d617862838b342b06d613a0ddf3 # v3.10.1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.HOMEBREW_TOKEN }}
|
||||||
|
org: anchore
|
||||||
|
formula: syft
|
||||||
|
tag: ${{ steps.release_info.outputs.tag_name }}
|
||||||
|
revision: ${{ steps.git_info.outputs.commit }}
|
||||||
|
|
||||||
|
- uses: 8398a7/action-slack@fbd6aa58ba854a740e11a35d0df80cb5d12101d8 #v3.15.1
|
||||||
|
if: ${{ failure() }}
|
||||||
|
with:
|
||||||
|
status: ${{ job.status }}
|
||||||
|
fields: repo,workflow,action,eventName
|
||||||
|
text: "Post-release homebrew publish workflow has failed: https://github.com/anchore/syft/actions/workflows/release-homebrew.yaml"
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||||
@@ -1,32 +1,30 @@
|
|||||||
name: "Release"
|
name: "Release"
|
||||||
on:
|
|
||||||
push:
|
|
||||||
# take no actions on push to any branch...
|
|
||||||
branches-ignore:
|
|
||||||
- "**"
|
|
||||||
# ... only act on release tags
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
|
|
||||||
env:
|
permissions:
|
||||||
GO_VERSION: "1.18.x"
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: tag the latest commit on main with the given version (prefixed with v)
|
||||||
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
quality-gate:
|
quality-gate:
|
||||||
environment: release
|
environment: release
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
|
||||||
# we don't want to release commits that have been pushed and tagged, but not necessarily merged onto main
|
- name: Check if tag already exists
|
||||||
- name: Ensure tagged commit is on main
|
# note: this will fail if the tag already exists
|
||||||
run: |
|
run: |
|
||||||
echo "Tag: ${GITHUB_REF##*/}"
|
[[ "${{ github.event.inputs.version }}" == v* ]] || (echo "version '${{ github.event.inputs.version }}' does not have a 'v' prefix" && exit 1)
|
||||||
git fetch origin main
|
git tag ${{ github.event.inputs.version }}
|
||||||
git merge-base --is-ancestor ${GITHUB_REF##*/} origin/main && echo "${GITHUB_REF##*/} is a commit on main!"
|
|
||||||
|
|
||||||
- name: Check static analysis results
|
- name: Check static analysis results
|
||||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
|
||||||
id: static-analysis
|
id: static-analysis
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -35,7 +33,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Check unit test results
|
- name: Check unit test results
|
||||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
|
||||||
id: unit
|
id: unit
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -44,7 +42,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Check integration test results
|
- name: Check integration test results
|
||||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
|
||||||
id: integration
|
id: integration
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -53,7 +51,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Check acceptance test results (linux)
|
- name: Check acceptance test results (linux)
|
||||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
|
||||||
id: acceptance-linux
|
id: acceptance-linux
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -62,7 +60,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Check acceptance test results (mac)
|
- name: Check acceptance test results (mac)
|
||||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
|
||||||
id: acceptance-mac
|
id: acceptance-mac
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -71,7 +69,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Check cli test results (linux)
|
- name: Check cli test results (linux)
|
||||||
uses: fountainhead/action-wait-for-check@v1.0.0
|
uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0
|
||||||
id: cli-linux
|
id: cli-linux
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -97,49 +95,37 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
with:
|
|
||||||
go-version: ${{ env.GO_VERSION }}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Restore tool cache
|
- name: Bootstrap environment
|
||||||
id: tool-cache
|
uses: ./.github/actions/bootstrap
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/.tmp
|
|
||||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
|
||||||
|
|
||||||
- name: Restore go cache
|
|
||||||
id: go-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
|
||||||
|
|
||||||
- name: (cache-miss) Bootstrap all project dependencies
|
|
||||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
|
||||||
run: make bootstrap
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d #v3.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.TOOLBOX_DOCKER_USER }}
|
username: ${{ secrets.TOOLBOX_DOCKER_USER }}
|
||||||
password: ${{ secrets.TOOLBOX_DOCKER_PASS }}
|
password: ${{ secrets.TOOLBOX_DOCKER_PASS }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d #v3.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Tag release
|
||||||
|
run: |
|
||||||
|
git config --global user.name "anchoreci"
|
||||||
|
git config --global user.email "anchoreci@users.noreply.github.com"
|
||||||
|
git tag -a ${{ github.event.inputs.version }} -m "Release ${{ github.event.inputs.version }}"
|
||||||
|
git push origin --tags
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build & publish release artifacts
|
- name: Build & publish release artifacts
|
||||||
run: make release
|
run: make ci-release
|
||||||
env:
|
env:
|
||||||
# for mac signing and notarization...
|
# for mac signing and notarization...
|
||||||
QUILL_SIGN_P12: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_CHAIN }}
|
QUILL_SIGN_P12: ${{ secrets.ANCHORE_APPLE_DEVELOPER_ID_CERT_CHAIN }}
|
||||||
@@ -155,23 +141,17 @@ jobs:
|
|||||||
AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
|
||||||
- uses: anchore/sbom-action@v0
|
- uses: anchore/sbom-action@c7f031d9249a826a082ea14c79d3b686a51d485a #v0.15.3
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
artifact-name: sbom.spdx.json
|
artifact-name: sbom.spdx.json
|
||||||
|
|
||||||
- uses: 8398a7/action-slack@v3
|
- uses: 8398a7/action-slack@fbd6aa58ba854a740e11a35d0df80cb5d12101d8 #v3.15.1
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
status: ${{ job.status }}
|
status: ${{ job.status }}
|
||||||
fields: repo,workflow,action,eventName
|
fields: repo,workflow,action,eventName
|
||||||
text: "A new Syft release has been published: https://github.com/anchore/syft/releases/tag/${{ github.ref_name }}"
|
text: "A new Syft release has been published: https://github.com/anchore/syft/releases/tag/${{ github.event.inputs.version }}"
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||||
if: ${{ success() }}
|
if: ${{ success() }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: artifacts
|
|
||||||
path: dist/**/*
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,79 +1,66 @@
|
|||||||
name: PR for latest versions of bootstrap tools
|
name: PR for latest versions of tools
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 8 * * *" # 3 AM EST
|
- cron: "0 8 * * *" # 3 AM EST
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
permissions:
|
||||||
GO_VERSION: "1.18.x"
|
contents: read
|
||||||
GO_STABLE_VERSION: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-bootstrap-tools:
|
update-bootstrap-tools:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'anchore/syft' # only run for main repo
|
if: github.repository == 'anchore/syft' # only run for main repo
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
|
||||||
- uses: actions/setup-go@v2
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
bootstrap-apt-packages: ""
|
||||||
stable: ${{ env.GO_STABLE_VERSION }}
|
compute-fingerprints: "false"
|
||||||
|
go-dependencies: false
|
||||||
|
|
||||||
- run: |
|
- name: "Update tool versions"
|
||||||
GOLANGCILINT_LATEST_VERSION=$(go list -m -json github.com/golangci/golangci-lint@latest 2>/dev/null | jq -r '.Version')
|
|
||||||
BOUNCER_LATEST_VERSION=$(go list -m -json github.com/wagoodman/go-bouncer@latest 2>/dev/null | jq -r '.Version')
|
|
||||||
CHRONICLE_LATEST_VERSION=$(go list -m -json github.com/anchore/chronicle@latest 2>/dev/null | jq -r '.Version')
|
|
||||||
GORELEASER_LATEST_VERSION=$(go list -m -json github.com/goreleaser/goreleaser@latest 2>/dev/null | jq -r '.Version')
|
|
||||||
GOSIMPORTS_LATEST_VERSION=$(go list -m -json github.com/rinchsan/gosimports@latest 2>/dev/null | jq -r '.Version')
|
|
||||||
YAJSV_LATEST_VERSION=$(go list -m -json github.com/neilpa/yajsv@latest 2>/dev/null | jq -r '.Version')
|
|
||||||
COSIGN_LATEST_VERSION=$(go list -m -json github.com/sigstore/cosign@latest 2>/dev/null | jq -r '.Version')
|
|
||||||
|
|
||||||
# update version variables in the Makefile
|
|
||||||
sed -r -i -e 's/^(GOLANGCILINT_VERSION = ).*/\1'${GOLANGCILINT_LATEST_VERSION}'/' Makefile
|
|
||||||
sed -r -i -e 's/^(BOUNCER_VERSION = ).*/\1'${BOUNCER_LATEST_VERSION}'/' Makefile
|
|
||||||
sed -r -i -e 's/^(CHRONICLE_VERSION = ).*/\1'${CHRONICLE_LATEST_VERSION}'/' Makefile
|
|
||||||
sed -r -i -e 's/^(GORELEASER_VERSION = ).*/\1'${GORELEASER_LATEST_VERSION}'/' Makefile
|
|
||||||
sed -r -i -e 's/^(GOSIMPORTS_VERSION = ).*/\1'${GOSIMPORTS_LATEST_VERSION}'/' Makefile
|
|
||||||
sed -r -i -e 's/^(YAJSV_VERSION = ).*/\1'${YAJSV_LATEST_VERSION}'/' Makefile
|
|
||||||
sed -r -i -e 's/^(COSIGN_VERSION = ).*/\1'${COSIGN_LATEST_VERSION}'/' Makefile
|
|
||||||
|
|
||||||
# update cosign in go.mod as well
|
|
||||||
go get github.com/sigstore/cosign@$COSIGN_LATEST_VERSION
|
|
||||||
go mod tidy
|
|
||||||
|
|
||||||
# export the versions for use with create-pull-request
|
|
||||||
echo "::set-output name=GOLANGCILINT::$GOLANGCILINT_LATEST_VERSION"
|
|
||||||
echo "::set-output name=BOUNCER::$BOUNCER_LATEST_VERSION"
|
|
||||||
echo "::set-output name=CHRONICLE::$CHRONICLE_LATEST_VERSION"
|
|
||||||
echo "::set-output name=GORELEASER::$GORELEASER_LATEST_VERSION"
|
|
||||||
echo "::set-output name=GOSIMPORTS::$GOSIMPORTS_LATEST_VERSION"
|
|
||||||
echo "::set-output name=YAJSV::$YAJSV_LATEST_VERSION"
|
|
||||||
echo "::set-output name=COSIGN::$COSIGN_LATEST_VERSION"
|
|
||||||
id: latest-versions
|
id: latest-versions
|
||||||
|
run: |
|
||||||
|
make update-tools
|
||||||
|
make list-tools
|
||||||
|
|
||||||
|
export NO_COLOR=1
|
||||||
|
delimiter="$(openssl rand -hex 8)"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "status<<${delimiter}"
|
||||||
|
make list-tool-updates
|
||||||
|
echo "${delimiter}"
|
||||||
|
} >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Tool version status"
|
||||||
|
echo "\`\`\`"
|
||||||
|
make list-tool-updates
|
||||||
|
echo "\`\`\`"
|
||||||
|
} >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
- uses: tibdex/github-app-token@v1
|
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
|
||||||
id: generate-token
|
id: generate-token
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- uses: peter-evans/create-pull-request@v4
|
- uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 #v5.0.2
|
||||||
with:
|
with:
|
||||||
signoff: true
|
signoff: true
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
branch: auto/latest-bootstrap-tools
|
branch: auto/latest-tools
|
||||||
labels: dependencies
|
labels: dependencies
|
||||||
commit-message: 'Update syft bootstrap tools to latest versions.'
|
commit-message: 'chore(deps): update tools to latest versions'
|
||||||
title: 'Update syft bootstrap tools to latest versions.'
|
title: 'chore(deps): update tools to latest versions'
|
||||||
body: |
|
body: |
|
||||||
- [golangci-lint ${{ steps.latest-versions.outputs.GOLANGCILINT }}](https://github.com/golangci/golangci-lint/releases/tag/${{ steps.latest-versions.outputs.GOLANGCILINT }})
|
```
|
||||||
- [bouncer ${{ steps.latest-versions.outputs.BOUNCER }}](https://github.com/wagoodman/go-bouncer/releases/tag/${{ steps.latest-versions.outputs.BOUNCER }})
|
${{ steps.latest-versions.outputs.status }}
|
||||||
- [chronicle ${{ steps.latest-versions.outputs.CHRONICLE }}](https://github.com/anchore/chronicle/releases/tag/${{ steps.latest-versions.outputs.CHRONICLE }})
|
```
|
||||||
- [goreleaser ${{ steps.latest-versions.outputs.GORELEASER }}](https://github.com/goreleaser/goreleaser/releases/tag/${{ steps.latest-versions.outputs.GORELEASER }})
|
This is an auto-generated pull request to update all of the tools to the latest versions.
|
||||||
- [gosimports ${{ steps.latest-versions.outputs.GOSIMPORTS }}](https://github.com/rinchsan/gosimports/releases/tag/${{ steps.latest-versions.outputs.GOSIMPORTS }})
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
- [yajsv ${{ steps.latest-versions.outputs.YAJSV }}](https://github.com/neilpa/yajsv/releases/tag/${{ steps.latest-versions.outputs.YAJSV }})
|
|
||||||
- [cosign ${{ steps.latest-versions.outputs.COSIGN }}](https://github.com/sigstore/cosign/releases/tag/${{ steps.latest-versions.outputs.COSIGN }})
|
|
||||||
This is an auto-generated pull request to update all of the bootstrap tools to the latest versions.
|
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: PR to update CPE dictionary index
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 1 * * 1" # every monday at 1 AM
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
SLACK_NOTIFICATIONS: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upgrade-cpe-dictionary-index:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'anchore/syft' # only run for main repo
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
|
||||||
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
|
|
||||||
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
|
|
||||||
|
- run: |
|
||||||
|
make generate-cpe-dictionary-index
|
||||||
|
|
||||||
|
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
|
||||||
|
id: generate-token
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||||
|
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 #v5.0.2
|
||||||
|
with:
|
||||||
|
signoff: true
|
||||||
|
delete-branch: true
|
||||||
|
branch: auto/latest-cpe-dictionary-index
|
||||||
|
labels: dependencies
|
||||||
|
commit-message: "chore(deps): update CPE dictionary index"
|
||||||
|
title: "chore(deps): update CPE dictionary index"
|
||||||
|
body: |
|
||||||
|
Update CPE dictionary index based on the latest available CPE dictionary
|
||||||
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
|
- uses: 8398a7/action-slack@fbd6aa58ba854a740e11a35d0df80cb5d12101d8 #v3.15.1
|
||||||
|
with:
|
||||||
|
status: ${{ job.status }}
|
||||||
|
fields: workflow,eventName,job
|
||||||
|
text: Syft CPE dictionary index update failed
|
||||||
|
env:
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
|
||||||
|
if: ${{ failure() && env.SLACK_NOTIFICATIONS == 'true' }}
|
||||||
@@ -6,21 +6,27 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.18.x"
|
GO_VERSION: "1.21.x"
|
||||||
GO_STABLE_VERSION: true
|
GO_STABLE_VERSION: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upgrade-stereoscope:
|
upgrade-stereoscope:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'anchore/syft' # only run for main repo
|
if: github.repository == 'anchore/syft' # only run for main repo
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 #v5.0.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
stable: ${{ env.GO_STABLE_VERSION }}
|
stable: ${{ env.GO_STABLE_VERSION }}
|
||||||
|
|
||||||
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
|
|
||||||
- run: |
|
- run: |
|
||||||
LATEST_VERSION=$(git ls-remote https://github.com/anchore/stereoscope main | head -n1 | awk '{print $1;}')
|
LATEST_VERSION=$(git ls-remote https://github.com/anchore/stereoscope main | head -n1 | awk '{print $1;}')
|
||||||
|
|
||||||
@@ -29,23 +35,23 @@ jobs:
|
|||||||
go mod tidy
|
go mod tidy
|
||||||
|
|
||||||
# export the version for use with create-pull-request
|
# export the version for use with create-pull-request
|
||||||
echo "::set-output name=LATEST_VERSION::$LATEST_VERSION"
|
echo "LATEST_VERSION=$LATEST_VERSION" >> $GITHUB_OUTPUT
|
||||||
id: latest-version
|
id: latest-version
|
||||||
|
|
||||||
- uses: tibdex/github-app-token@v1
|
- uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a #v2.1.0
|
||||||
id: generate-token
|
id: generate-token
|
||||||
with:
|
with:
|
||||||
app_id: ${{ secrets.TOKEN_APP_ID }}
|
app_id: ${{ secrets.TOKEN_APP_ID }}
|
||||||
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
private_key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- uses: peter-evans/create-pull-request@v4
|
- uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 #v5.0.2
|
||||||
with:
|
with:
|
||||||
signoff: true
|
signoff: true
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
branch: auto/latest
|
branch: auto/latest-stereoscope
|
||||||
labels: dependencies
|
labels: dependencies
|
||||||
commit-message: "Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
|
commit-message: "chore(deps): update stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
|
||||||
title: "Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
|
title: "chore(deps): update stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}"
|
||||||
body: |
|
body: |
|
||||||
Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}
|
Update Stereoscope to ${{ steps.latest-version.outputs.LATEST_VERSION }}
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|||||||
+111
-267
@@ -1,14 +1,14 @@
|
|||||||
name: "Validations"
|
name: "Validations"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
|
||||||
|
|
||||||
env:
|
permissions:
|
||||||
GO_VERSION: "1.18.x"
|
contents: read
|
||||||
GO_STABLE_VERSION: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
@@ -17,155 +17,74 @@ jobs:
|
|||||||
name: "Static analysis"
|
name: "Static analysis"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
with:
|
|
||||||
go-version: ${{ env.GO_VERSION }}
|
|
||||||
stable: ${{ env.GO_STABLE_VERSION }}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
- name: Restore tool cache
|
|
||||||
id: tool-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/.tmp
|
|
||||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
|
||||||
|
|
||||||
- name: Restore go cache
|
|
||||||
id: go-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
|
||||||
|
|
||||||
- name: (cache-miss) Bootstrap all project dependencies
|
|
||||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
|
||||||
run: make bootstrap
|
|
||||||
|
|
||||||
- name: Bootstrap CI environment dependencies
|
|
||||||
run: make ci-bootstrap
|
|
||||||
|
|
||||||
- name: Run static analysis
|
- name: Run static analysis
|
||||||
run: make static-analysis
|
run: make static-analysis
|
||||||
|
|
||||||
|
|
||||||
Unit-Test:
|
Unit-Test:
|
||||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||||
name: "Unit tests"
|
name: "Unit tests"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
with:
|
|
||||||
go-version: ${{ env.GO_VERSION }}
|
|
||||||
stable: ${{ env.GO_STABLE_VERSION }}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
- name: Restore tool cache
|
|
||||||
id: tool-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/.tmp
|
|
||||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
|
||||||
|
|
||||||
- name: Restore go cache
|
|
||||||
id: go-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
|
||||||
|
|
||||||
- name: (cache-miss) Bootstrap all project dependencies
|
|
||||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
|
||||||
run: make bootstrap
|
|
||||||
|
|
||||||
- name: Bootstrap CI environment dependencies
|
|
||||||
run: make ci-bootstrap
|
|
||||||
|
|
||||||
- name: Build cache key for java test-fixture blobs (for unit tests)
|
|
||||||
run: make java-packages-fingerprint
|
|
||||||
|
|
||||||
- name: Restore Java test-fixture cache
|
- name: Restore Java test-fixture cache
|
||||||
id: unit-java-cache
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
with:
|
||||||
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
|
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
|
||||||
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
|
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint' ) }}
|
||||||
|
|
||||||
- name: Build cache key for rpm test-fixture blobs (for unit tests)
|
|
||||||
run: make rpm-binaries-fingerprint
|
|
||||||
|
|
||||||
- name: Restore RPM test-fixture cache
|
- name: Restore RPM test-fixture cache
|
||||||
id: unit-rpm-cache
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
with:
|
||||||
path: syft/pkg/cataloger/rpm/test-fixtures/rpms
|
path: syft/pkg/cataloger/redhat/test-fixtures/rpms
|
||||||
key: ${{ runner.os }}-unit-rpm-cache-${{ hashFiles( 'syft/pkg/cataloger/rpm/test-fixtures/rpms.fingerprint' ) }}
|
key: ${{ runner.os }}-unit-rpm-cache-${{ hashFiles( 'syft/pkg/cataloger/redhat/test-fixtures/rpms.fingerprint' ) }}
|
||||||
|
|
||||||
- name: Build cache key for go binary test-fixture blobs (for unit tests)
|
- name: Restore go binary test-fixture cache
|
||||||
run: make go-binaries-fingerprint
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
|
|
||||||
- name: Restore Go binary test-fixture cache
|
|
||||||
id: unit-go-binary-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
with:
|
||||||
path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries
|
path: syft/pkg/cataloger/golang/test-fixtures/archs/binaries
|
||||||
key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }}
|
key: ${{ runner.os }}-unit-go-binaries-cache-${{ hashFiles( 'syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint' ) }}
|
||||||
|
|
||||||
|
- name: Restore binary cataloger test-fixture cache
|
||||||
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
|
with:
|
||||||
|
path: syft/pkg/cataloger/binary/test-fixtures/classifiers/bin
|
||||||
|
key: ${{ runner.os }}-unit-binary-cataloger-cache-${{ hashFiles( 'syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint' ) }}
|
||||||
|
|
||||||
|
- name: Restore Kernel test-fixture cache
|
||||||
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
|
with:
|
||||||
|
path: syft/pkg/cataloger/kernel/test-fixtures/cache
|
||||||
|
key: ${{ runner.os }}-unit-kernel-cache-${{ hashFiles( 'syft/pkg/cataloger/kernel/test-fixtures/cache.fingerprint' ) }}
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: make unit
|
run: make unit
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: unit-test-results
|
|
||||||
path: test/results/**/*
|
|
||||||
|
|
||||||
Integration-Test:
|
Integration-Test:
|
||||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||||
name: "Integration tests"
|
name: "Integration tests"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
with:
|
|
||||||
go-version: ${{ env.GO_VERSION }}
|
|
||||||
stable: ${{ env.GO_STABLE_VERSION }}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
- name: Restore tool cache
|
|
||||||
id: tool-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/.tmp
|
|
||||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
|
||||||
|
|
||||||
- name: Restore go cache
|
|
||||||
id: go-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
|
||||||
|
|
||||||
- name: (cache-miss) Bootstrap all project dependencies
|
|
||||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
|
||||||
run: make bootstrap
|
|
||||||
|
|
||||||
- name: Bootstrap CI environment dependencies
|
|
||||||
run: make ci-bootstrap
|
|
||||||
|
|
||||||
- name: Validate syft output against the CycloneDX schema
|
- name: Validate syft output against the CycloneDX schema
|
||||||
run: make validate-cyclonedx-schema
|
run: make validate-cyclonedx-schema
|
||||||
|
|
||||||
- name: Build key for tar cache
|
|
||||||
run: make integration-fingerprint
|
|
||||||
|
|
||||||
- name: Restore integration test cache
|
- name: Restore integration test cache
|
||||||
uses: actions/cache@v2.1.3
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
|
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
|
||||||
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
|
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
|
||||||
@@ -173,123 +92,34 @@ jobs:
|
|||||||
- name: Run integration tests
|
- name: Run integration tests
|
||||||
run: make integration
|
run: make integration
|
||||||
|
|
||||||
Benchmark-Test:
|
|
||||||
name: "Benchmark tests"
|
|
||||||
runs-on: ubuntu-20.04
|
|
||||||
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
|
|
||||||
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
|
|
||||||
# the job by event.
|
|
||||||
steps:
|
|
||||||
- uses: actions/setup-go@v2
|
|
||||||
with:
|
|
||||||
go-version: ${{ env.GO_VERSION }}
|
|
||||||
stable: ${{ env.GO_STABLE_VERSION }}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Restore tool cache
|
|
||||||
id: tool-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/.tmp
|
|
||||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
|
||||||
|
|
||||||
- name: Restore go cache
|
|
||||||
id: go-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
|
||||||
|
|
||||||
- name: (cache-miss) Bootstrap all project dependencies
|
|
||||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
|
||||||
run: make bootstrap
|
|
||||||
|
|
||||||
- name: Bootstrap CI environment dependencies
|
|
||||||
run: make ci-bootstrap
|
|
||||||
|
|
||||||
- name: Restore base benchmark result
|
|
||||||
uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: test/results/benchmark-main.txt
|
|
||||||
# use base sha for PR or new commit hash for main push in benchmark result key
|
|
||||||
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
|
|
||||||
|
|
||||||
- name: Run benchmark tests
|
|
||||||
id: benchmark
|
|
||||||
run: |
|
|
||||||
REF_NAME=${GITHUB_REF##*/} make benchmark
|
|
||||||
OUTPUT=$(make show-benchstat)
|
|
||||||
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
|
|
||||||
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
|
|
||||||
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
|
|
||||||
echo "::set-output name=result::$OUTPUT"
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: benchmark-test-results
|
|
||||||
path: test/results/**/*
|
|
||||||
|
|
||||||
- name: Update PR benchmark results comment
|
|
||||||
uses: marocchino/sticky-pull-request-comment@v2
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
header: benchmark
|
|
||||||
message: |
|
|
||||||
### Benchmark Test Results
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Benchmark results from the latest changes vs base branch</summary>
|
|
||||||
|
|
||||||
```
|
|
||||||
${{ steps.benchmark.outputs.result }}
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
Build-Snapshot-Artifacts:
|
Build-Snapshot-Artifacts:
|
||||||
name: "Build snapshot artifacts"
|
name: "Build snapshot artifacts"
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
|
||||||
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
bootstrap-apt-packages: ""
|
||||||
stable: ${{ env.GO_STABLE_VERSION }}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v1
|
|
||||||
|
|
||||||
- name: Restore tool cache
|
|
||||||
id: tool-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/.tmp
|
|
||||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
|
||||||
|
|
||||||
- name: Restore go cache
|
|
||||||
id: go-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
|
||||||
|
|
||||||
- name: (cache-miss) Bootstrap all project dependencies
|
|
||||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
|
||||||
run: make bootstrap
|
|
||||||
|
|
||||||
- name: Build snapshot artifacts
|
- name: Build snapshot artifacts
|
||||||
run: make snapshot
|
run: make snapshot
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2
|
# why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach).
|
||||||
|
# see https://github.com/actions/upload-artifact/issues/199 for more info
|
||||||
|
- name: Upload snapshot artifacts
|
||||||
|
uses: actions/cache/save@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
with:
|
with:
|
||||||
name: artifacts
|
# we need to preserve the snapshot data itself as well as the task data that confirms if the
|
||||||
path: snapshot/**/*
|
# snapshot build is stale or not. Otherwise the downstream jobs will attempt to rebuild the snapshot
|
||||||
|
# even though it already exists.
|
||||||
|
path: |
|
||||||
|
snapshot
|
||||||
|
.task
|
||||||
|
key: snapshot-build-${{ github.run_id }}
|
||||||
|
|
||||||
|
|
||||||
Acceptance-Linux:
|
Acceptance-Linux:
|
||||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||||
@@ -297,22 +127,32 @@ jobs:
|
|||||||
needs: [Build-Snapshot-Artifacts]
|
needs: [Build-Snapshot-Artifacts]
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
|
||||||
- uses: actions/download-artifact@v2
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
|
|
||||||
|
- name: Download snapshot build
|
||||||
|
id: snapshot-cache
|
||||||
|
uses: actions/cache/restore@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
with:
|
with:
|
||||||
name: artifacts
|
path: |
|
||||||
path: snapshot
|
snapshot
|
||||||
|
.task
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
key: snapshot-build-${{ github.run_id }}
|
||||||
|
|
||||||
|
# workaround for https://github.com/actions/cache/issues/1265
|
||||||
|
- name: (cache-miss) Snapshot build missing
|
||||||
|
if: steps.snapshot-cache.outputs.cache-hit != 'true'
|
||||||
|
run: echo "unable to download snapshots from previous job" && false
|
||||||
|
|
||||||
- name: Run comparison tests (Linux)
|
- name: Run comparison tests (Linux)
|
||||||
run: make compare-linux
|
run: make compare-linux
|
||||||
|
|
||||||
- name: Build key for image cache
|
|
||||||
run: make install-fingerprint
|
|
||||||
|
|
||||||
- name: Restore install.sh test image cache
|
- name: Restore install.sh test image cache
|
||||||
id: install-test-image-cache
|
id: install-test-image-cache
|
||||||
uses: actions/cache@v2.1.3
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/test/install/cache
|
path: ${{ github.workspace }}/test/install/cache
|
||||||
key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }}
|
key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }}
|
||||||
@@ -335,16 +175,34 @@ jobs:
|
|||||||
needs: [Build-Snapshot-Artifacts]
|
needs: [Build-Snapshot-Artifacts]
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
|
|
||||||
- uses: actions/download-artifact@v2
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
with:
|
with:
|
||||||
name: artifacts
|
bootstrap-apt-packages: ""
|
||||||
path: snapshot
|
compute-fingerprints: "false"
|
||||||
|
go-dependencies: false
|
||||||
|
go-version: ""
|
||||||
|
|
||||||
|
- name: Download snapshot build
|
||||||
|
id: snapshot-cache
|
||||||
|
uses: actions/cache/restore@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
snapshot
|
||||||
|
.task
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
key: snapshot-build-${{ github.run_id }}
|
||||||
|
|
||||||
|
# workaround for https://github.com/actions/cache/issues/1265
|
||||||
|
- name: (cache-miss) Snapshot build missing
|
||||||
|
if: steps.snapshot-cache.outputs.cache-hit != 'true'
|
||||||
|
run: echo "unable to download snapshots from previous job" && false
|
||||||
|
|
||||||
- name: Restore docker image cache for compare testing
|
- name: Restore docker image cache for compare testing
|
||||||
id: mac-compare-testing-cache
|
id: mac-compare-testing-cache
|
||||||
uses: actions/cache@v2.1.3
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
with:
|
with:
|
||||||
path: image.tar
|
path: image.tar
|
||||||
key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }}
|
key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }}
|
||||||
@@ -355,52 +213,38 @@ jobs:
|
|||||||
- name: Run install.sh tests (Mac)
|
- name: Run install.sh tests (Mac)
|
||||||
run: make install-test-ci-mac
|
run: make install-test-ci-mac
|
||||||
|
|
||||||
|
|
||||||
Cli-Linux:
|
Cli-Linux:
|
||||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||||
name: "CLI tests (Linux)"
|
name: "CLI tests (Linux)"
|
||||||
needs: [Build-Snapshot-Artifacts]
|
needs: [Build-Snapshot-Artifacts]
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1
|
||||||
with:
|
|
||||||
go-version: ${{ env.GO_VERSION }}
|
|
||||||
stable: ${{ env.GO_STABLE_VERSION }}
|
|
||||||
|
|
||||||
- uses: actions/checkout@v2
|
- name: Bootstrap environment
|
||||||
|
uses: ./.github/actions/bootstrap
|
||||||
|
|
||||||
- name: Restore go cache
|
- name: Restore CLI test-fixture cache
|
||||||
id: go-cache
|
uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
|
||||||
|
|
||||||
- name: Restore tool cache
|
|
||||||
id: tool-cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
|
||||||
path: ${{ github.workspace }}/.tmp
|
|
||||||
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
|
||||||
|
|
||||||
- name: (cache-miss) Bootstrap all project dependencies
|
|
||||||
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
|
||||||
run: make bootstrap
|
|
||||||
|
|
||||||
- name: Build key for tar cache
|
|
||||||
run: make cli-fingerprint
|
|
||||||
|
|
||||||
- name: Restore CLI test cache
|
|
||||||
uses: actions/cache@v2.1.3
|
|
||||||
with:
|
with:
|
||||||
path: ${{ github.workspace }}/test/cli/test-fixtures/cache
|
path: ${{ github.workspace }}/test/cli/test-fixtures/cache
|
||||||
key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }}
|
key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }}
|
||||||
|
|
||||||
- uses: actions/download-artifact@v2
|
- name: Download snapshot build
|
||||||
|
id: snapshot-cache
|
||||||
|
uses: actions/cache/restore@e12d46a63a90f2fae62d114769bbf2a179198b5c #v3.3.3
|
||||||
with:
|
with:
|
||||||
name: artifacts
|
path: |
|
||||||
path: snapshot
|
snapshot
|
||||||
|
.task
|
||||||
|
fail-on-cache-miss: true
|
||||||
|
key: snapshot-build-${{ github.run_id }}
|
||||||
|
|
||||||
|
# workaround for https://github.com/actions/cache/issues/1265
|
||||||
|
- name: (cache-miss) Snapshot build missing
|
||||||
|
if: steps.snapshot-cache.outputs.cache-hit != 'true'
|
||||||
|
run: echo "unable to download snapshots from previous job" && false
|
||||||
|
|
||||||
- name: Run CLI Tests (Linux)
|
- name: Run CLI Tests (Linux)
|
||||||
run: make cli
|
run: make cli
|
||||||
|
|||||||
+39
-9
@@ -1,11 +1,41 @@
|
|||||||
CHANGELOG.md
|
# local development tailoring
|
||||||
/test/results
|
go.work
|
||||||
|
go.work.sum
|
||||||
|
.tool-versions
|
||||||
|
|
||||||
|
# app configuration
|
||||||
|
/.syft.yaml
|
||||||
|
|
||||||
|
# tool and bin directories
|
||||||
|
.tmp/
|
||||||
|
bin/
|
||||||
|
/bin
|
||||||
|
/.bin
|
||||||
|
/build
|
||||||
/dist
|
/dist
|
||||||
/snapshot
|
/snapshot
|
||||||
.server/
|
/.tool
|
||||||
|
/.task
|
||||||
|
|
||||||
|
# changelog generation
|
||||||
|
CHANGELOG.md
|
||||||
|
VERSION
|
||||||
|
|
||||||
|
# IDE configuration
|
||||||
.vscode/
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.server/
|
||||||
.history/
|
.history/
|
||||||
|
|
||||||
|
# test related
|
||||||
*.fingerprint
|
*.fingerprint
|
||||||
|
/test/results
|
||||||
|
coverage.txt
|
||||||
|
*.log
|
||||||
|
test/integration/test-fixtures/**/go.sum
|
||||||
|
|
||||||
|
# probable archives
|
||||||
|
.images
|
||||||
*.tar
|
*.tar
|
||||||
*.jar
|
*.jar
|
||||||
*.war
|
*.war
|
||||||
@@ -13,12 +43,7 @@ CHANGELOG.md
|
|||||||
*.jpi
|
*.jpi
|
||||||
*.hpi
|
*.hpi
|
||||||
*.zip
|
*.zip
|
||||||
.idea/
|
*.iml
|
||||||
*.log
|
|
||||||
.images
|
|
||||||
.tmp/
|
|
||||||
coverage.txt
|
|
||||||
bin/
|
|
||||||
|
|
||||||
# Binaries for programs and plugins
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
@@ -41,3 +66,8 @@ bin/
|
|||||||
# attestation
|
# attestation
|
||||||
cosign.key
|
cosign.key
|
||||||
cosign.pub
|
cosign.pub
|
||||||
|
|
||||||
|
# Byte-compiled object files for python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|||||||
+18
-11
@@ -1,5 +1,7 @@
|
|||||||
# TODO: enable this when we have coverage on docstring comments
|
issues:
|
||||||
#issues:
|
max-same-issues: 25
|
||||||
|
|
||||||
|
# TODO: enable this when we have coverage on docstring comments
|
||||||
# # The list of ids of default excludes to include or disable.
|
# # The list of ids of default excludes to include or disable.
|
||||||
# include:
|
# include:
|
||||||
# - EXC0002 # disable excluding of issues about comments from golint
|
# - EXC0002 # disable excluding of issues about comments from golint
|
||||||
@@ -10,10 +12,9 @@ linters:
|
|||||||
enable:
|
enable:
|
||||||
- asciicheck
|
- asciicheck
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- depguard
|
- dogsled
|
||||||
- dupl
|
- dupl
|
||||||
- errcheck
|
- errcheck
|
||||||
- errorlint
|
|
||||||
- exportloopref
|
- exportloopref
|
||||||
- funlen
|
- funlen
|
||||||
- gocognit
|
- gocognit
|
||||||
@@ -21,14 +22,14 @@ linters:
|
|||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- gofmt
|
- gofmt
|
||||||
- tparallel
|
- goimports
|
||||||
- importas
|
- goprintffuncname
|
||||||
- gosec
|
- gosec
|
||||||
- gosimple
|
- gosimple
|
||||||
- govet
|
- govet
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- misspell
|
- misspell
|
||||||
- nolintlint
|
- nakedret
|
||||||
- revive
|
- revive
|
||||||
- staticcheck
|
- staticcheck
|
||||||
- stylecheck
|
- stylecheck
|
||||||
@@ -37,6 +38,7 @@ linters:
|
|||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
- whitespace
|
- whitespace
|
||||||
|
|
||||||
linters-settings:
|
linters-settings:
|
||||||
funlen:
|
funlen:
|
||||||
# Checks the number of lines in a function.
|
# Checks the number of lines in a function.
|
||||||
@@ -53,7 +55,8 @@ run:
|
|||||||
timeout: 10m
|
timeout: 10m
|
||||||
|
|
||||||
# do not enable...
|
# do not enable...
|
||||||
# - dogsled # found to be to niche and ineffective
|
# - deadcode # The owner seems to have abandoned the linter. Replaced by "unused".
|
||||||
|
# - depguard # We don't have a configuration for this yet
|
||||||
# - goprintffuncname # does not catch all cases and there are exceptions
|
# - goprintffuncname # does not catch all cases and there are exceptions
|
||||||
# - nakedret # does not catch all cases and should not fail a build
|
# - nakedret # does not catch all cases and should not fail a build
|
||||||
# - gochecknoglobals
|
# - gochecknoglobals
|
||||||
@@ -69,7 +72,11 @@ run:
|
|||||||
# - lll # without a way to specify per-line exception cases, this is not usable
|
# - lll # without a way to specify per-line exception cases, this is not usable
|
||||||
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
|
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
|
||||||
# - nestif
|
# - nestif
|
||||||
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
|
# - nolintlint # as of go1.19 this conflicts with the behavior of gofmt, which is a deal-breaker (lint-fix will still fail when running lint)
|
||||||
# - scopelint # deprecated
|
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
|
||||||
|
# - rowserrcheck # not in a repo with sql, so this is not useful
|
||||||
|
# - scopelint # deprecated
|
||||||
|
# - structcheck # The owner seems to have abandoned the linter. Replaced by "unused".
|
||||||
# - testpackage
|
# - testpackage
|
||||||
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
|
# - varcheck # The owner seems to have abandoned the linter. Replaced by "unused".
|
||||||
|
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
|
||||||
|
|||||||
+6
-9
@@ -5,6 +5,7 @@ release:
|
|||||||
env:
|
env:
|
||||||
# required to support multi architecture docker builds
|
# required to support multi architecture docker builds
|
||||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: linux-build
|
- id: linux-build
|
||||||
@@ -19,16 +20,14 @@ builds:
|
|||||||
- s390x
|
- s390x
|
||||||
# set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build
|
# set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build
|
||||||
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
|
mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}'
|
||||||
env: &build-env
|
|
||||||
- CGO_ENABLED=0
|
|
||||||
ldflags: &build-ldflags |
|
ldflags: &build-ldflags |
|
||||||
-w
|
-w
|
||||||
-s
|
-s
|
||||||
-extldflags '-static'
|
-extldflags '-static'
|
||||||
-X github.com/anchore/syft/internal/version.version={{.Version}}
|
-X main.version={{.Version}}
|
||||||
-X github.com/anchore/syft/internal/version.gitCommit={{.Commit}}
|
-X main.gitCommit={{.Commit}}
|
||||||
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
|
-X main.buildDate={{.Date}}
|
||||||
-X github.com/anchore/syft/internal/version.gitDescription={{.Summary}}
|
-X main.gitDescription={{.Summary}}
|
||||||
|
|
||||||
- id: darwin-build
|
- id: darwin-build
|
||||||
dir: ./cmd/syft
|
dir: ./cmd/syft
|
||||||
@@ -39,11 +38,10 @@ builds:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
mod_timestamp: *build-timestamp
|
mod_timestamp: *build-timestamp
|
||||||
env: *build-env
|
|
||||||
ldflags: *build-ldflags
|
ldflags: *build-ldflags
|
||||||
hooks:
|
hooks:
|
||||||
post:
|
post:
|
||||||
- cmd: .tmp/quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv
|
- cmd: .tool/quill sign-and-notarize "{{ .Path }}" --dry-run={{ .IsSnapshot }} --ad-hoc={{ .IsSnapshot }} -vv
|
||||||
env:
|
env:
|
||||||
- QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log
|
- QUILL_LOG_FILE=/tmp/quill-{{ .Target }}.log
|
||||||
|
|
||||||
@@ -55,7 +53,6 @@ builds:
|
|||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
mod_timestamp: *build-timestamp
|
mod_timestamp: *build-timestamp
|
||||||
env: *build-env
|
|
||||||
ldflags: *build-ldflags
|
ldflags: *build-ldflags
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
# Contributing to Syft
|
[#](#) Contributing to Syft
|
||||||
|
|
||||||
If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
|
If you are looking to contribute to this project and want to open a GitHub pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ Before you can contribute to Syft, you need to configure your development enviro
|
|||||||
|
|
||||||
You will need to install Go. The version on https://go.dev works best, using the system golang doesn't always work the way you might expect.
|
You will need to install Go. The version on https://go.dev works best, using the system golang doesn't always work the way you might expect.
|
||||||
|
|
||||||
At the time of writing, Go 1.19 does not work correctly with Syft. Please use Go 1.18 for now.
|
Refer to the go.mod file in the root of this repo for the recommended version of Go to install.
|
||||||
|
|
||||||
You will also need Docker. There's no reason the system packages shouldn't work, but we used the official Docker package. You can find instructions for installing Docker in Debian [here](https://docs.docker.com/engine/install/debian/).
|
You will also need Docker. There's no reason the system packages shouldn't work, but we used the official Docker package. You can find instructions for installing Docker in Debian [here](https://docs.docker.com/engine/install/debian/).
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Syft Contributors
|
||||||
|
|
||||||
|
The following Syft components were contributed by external authors/organizations.
|
||||||
|
|
||||||
|
## GraalVM Native Image
|
||||||
|
|
||||||
|
A cataloger contributed by Oracle Corporation that extracts packages given within GraalVM Native Image SBOMs.
|
||||||
|
|
||||||
|
## Swift Package Manager
|
||||||
|
|
||||||
|
A cataloger contributed by Axis Communications that catalogs packages resolved by Swift Package Manager.
|
||||||
+159
-67
@@ -6,6 +6,26 @@ In order to test and develop in this repo you will need the following dependenci
|
|||||||
- Golang
|
- Golang
|
||||||
- docker
|
- docker
|
||||||
- make
|
- make
|
||||||
|
- Python (>= 3.9)
|
||||||
|
|
||||||
|
### Docker settings for getting started
|
||||||
|
Make sure you've updated your docker settings so the default docker socket path is available.
|
||||||
|
|
||||||
|
Go to:
|
||||||
|
|
||||||
|
docker -> settings -> advanced
|
||||||
|
|
||||||
|
Make sure:
|
||||||
|
|
||||||
|
```
|
||||||
|
Allow the default Docker socket to be used
|
||||||
|
```
|
||||||
|
|
||||||
|
is checked.
|
||||||
|
|
||||||
|
Also double check that the docker context being used is the default context. If it is not, run:
|
||||||
|
|
||||||
|
`docker context use default`
|
||||||
|
|
||||||
After cloning the following step can help you get setup:
|
After cloning the following step can help you get setup:
|
||||||
1. run `make bootstrap` to download go mod dependencies, create the `/.tmp` dir, and download helper utilities.
|
1. run `make bootstrap` to download go mod dependencies, create the `/.tmp` dir, and download helper utilities.
|
||||||
@@ -15,63 +35,55 @@ After cloning the following step can help you get setup:
|
|||||||
- this command `go run cmd/syft/main.go alpine:latest` will compile and run syft against `alpine:latest`
|
- this command `go run cmd/syft/main.go alpine:latest` will compile and run syft against `alpine:latest`
|
||||||
5. view the README or syft help output for more output options
|
5. view the README or syft help output for more output options
|
||||||
|
|
||||||
#### Make output
|
The main make tasks for common static analysis and testing are `lint`, `format`, `lint-fix`, `unit`, `integration`, and `cli`.
|
||||||
The main make tasks for common static analysis and testing are `lint`, `lint-fix`, `unit`, `integration`, and `cli`.
|
|
||||||
|
See `make help` for all the current make tasks.
|
||||||
|
|
||||||
|
### Internal Artifactory Settings
|
||||||
|
|
||||||
|
**Not always applicable**
|
||||||
|
|
||||||
|
Some companies have Artifactory setup internally as a solution for sourcing secure dependencies.
|
||||||
|
If you're seeing an issue where the unit tests won't run because of the below error then this section might be relevant for your use case.
|
||||||
|
|
||||||
```
|
```
|
||||||
all Run all linux-based checks (linting, license check, unit, integration, and linux compare tests)
|
[ERROR] [ERROR] Some problems were encountered while processing the POMs
|
||||||
benchmark Run benchmark tests and compare against the baseline (if available)
|
|
||||||
bootstrap Download and install all tooling dependencies (+ prep tooling in the ./tmp dir)
|
|
||||||
build Build release snapshot binaries and packages
|
|
||||||
check-licenses Ensure transitive dependencies are compliant with the current license policy
|
|
||||||
clean-test-image-cache Clean test image cache
|
|
||||||
clean Remove previous builds, result reports, and test cache
|
|
||||||
cli Run CLI tests
|
|
||||||
compare-linux Run compare tests on build snapshot binaries and packages (Linux)
|
|
||||||
compare-mac Run compare tests on build snapshot binaries and packages (Mac)
|
|
||||||
generate-json-schema Generate a new json schema
|
|
||||||
generate-license-list Generate an updated spdx license list
|
|
||||||
help Display this help
|
|
||||||
integration Run integration tests
|
|
||||||
lint-fix Auto-format all source code + run golangci lint fixers
|
|
||||||
lint Run gofmt + golangci lint checks
|
|
||||||
show-test-image-cache Show all docker and image tar cache
|
|
||||||
show-test-snapshots Show all test snapshots
|
|
||||||
snapshot-with-signing Build snapshot release binaries and packages (with dummy signing)
|
|
||||||
test Run all tests (currently unit, integration, linux compare, and cli tests)
|
|
||||||
unit Run unit tests (with coverage)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you're dealing with an issue where the unit tests will not pull/build certain java fixtures check some of these settings:
|
||||||
|
|
||||||
|
- a `settings.xml` file should be available to help you communicate with your internal artifactory deployment
|
||||||
|
- this can be moved to `syft/pkg/cataloger/java/test-fixtures/java-builds/example-jenkins-plugin/` to help build the unit test-fixtures
|
||||||
|
- you'll also want to modify the `build-example-jenkins-plugin.sh` to use `settings.xml`
|
||||||
|
|
||||||
|
For more information on this setup and troubleshooting see [issue 1895](https://github.com/anchore/syft/issues/1895#issuecomment-1610085319)
|
||||||
|
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Syft is used to generate a Software Bill of Materials (SBOM) from different kinds of input.
|
At a high level, this is the package structure of syft:
|
||||||
|
|
||||||
### Code organization for the cmd package
|
|
||||||
Syft's entrypoint can be found in the `cmd` package at `cmd/syft/main.go`. `main.go` builds a new syft `cli` via `cli.New()`
|
|
||||||
and then executes the `cli` via `cli.Execute()`. The `cli` package is responsible for parsing command line arguments,
|
|
||||||
setting up the application context and configuration, and executing the application. Each of syft's commands
|
|
||||||
(e.g. `packages`, `attest`, `version`) are implemented as a `cobra.Command` in their respective `<command>.go` files.
|
|
||||||
They are registered in `syft/cli/commands/go`.
|
|
||||||
```
|
```
|
||||||
.
|
./cmd/syft/
|
||||||
└── syft/
|
│ ├── cli/
|
||||||
├── cli/
|
│ │ ├── cli.go // where all commands are wired up
|
||||||
│ ├── attest/
|
│ │ ├── commands/ // all command implementations
|
||||||
│ ├── attest.go
|
│ │ ├── options/ // all command flags and configuration options
|
||||||
│ ├── commands.go
|
│ │ └── ui/ // all handlers for events that are shown on the UI
|
||||||
│ ├── completion.go
|
│ └── main.go // entrypoint for the application
|
||||||
│ ├── convert/
|
└── syft/ // the "core" syft library
|
||||||
│ ├── convert.go
|
├── format/ // contains code to encode or decode to and from SBOM formats
|
||||||
│ ├── eventloop/
|
├── pkg/ // contains code to catalog packages from a source
|
||||||
│ ├── options/
|
├── sbom/ // contains the definition of an SBOM
|
||||||
│ ├── packages/
|
└── source/ // contains code to create a source object for some input type (e.g. container image, directory, etc)
|
||||||
│ ├── packages.go
|
|
||||||
│ ├── poweruser/
|
|
||||||
│ ├── poweruser.go
|
|
||||||
│ └── version.go
|
|
||||||
└── main.go
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Execution flow
|
Syft's core library is implemented in the `syft` package and subpackages, where the major packages are:
|
||||||
|
|
||||||
|
- the `syft/source` package produces a `source.Source` object that can be used to catalog a directory, container, and other source types.
|
||||||
|
- the `syft` package contains a single function that can take a `source.Source` object and catalog it, producing an `sbom.SBOM` object
|
||||||
|
- the `syft/format` package contains the ability to encode and decode SBOMs to and from different SBOM formats (such as SPDX and CycloneDX)
|
||||||
|
|
||||||
|
The `cmd` pacakge at the highest level execution flow wires up [`spf13/cobra`](https://github.com/spf13/cobra) commands for execution in the main application:
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant main as cmd/syft/main
|
participant main as cmd/syft/main
|
||||||
@@ -95,24 +107,7 @@ sequenceDiagram
|
|||||||
Note right of cmd: Execute SINGLE command from USER
|
Note right of cmd: Execute SINGLE command from USER
|
||||||
```
|
```
|
||||||
|
|
||||||
### Code organization for syft library
|
The `packages` command uses the core library to generate an SBOM for the given user input:
|
||||||
|
|
||||||
Syft's core library (see, exported) functionality is implemented in the `syft` package. The `syft` package is responsible for organizing the core
|
|
||||||
SBOM data model, it's translated output formats, and the core SBOM generation logic.
|
|
||||||
|
|
||||||
#### Organization and design notes for the syft library
|
|
||||||
- analysis creates a static SBOM which can be encoded and decoded
|
|
||||||
- format objects, should strive to not add or enrich data in encoding that could otherwise be done during analysis
|
|
||||||
- package catalogers and their organization can be viewed/added to the `syft/pkg/cataloger` package
|
|
||||||
- file catalogers and their organization can be viewed/added to the `syft/file` package
|
|
||||||
- The source package provides an abstraction to allow a user to loosely define a data source that can be cataloged
|
|
||||||
- Logging Abstraction ...
|
|
||||||
|
|
||||||
#### Code example of syft as a library
|
|
||||||
Here is a gist of using syft as a library to generate a SBOM from a docker image: [link](https://gist.github.com/wagoodman/57ed59a6d57600c23913071b8470175b).
|
|
||||||
The execution flow for the example is detailed below.
|
|
||||||
|
|
||||||
#### Execution flow examples for the syft library
|
|
||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant source as source.New(ubuntu:latest)
|
participant source as source.New(ubuntu:latest)
|
||||||
@@ -131,8 +126,105 @@ sequenceDiagram
|
|||||||
Note right of catalog: cataloger configuration is done based on src
|
Note right of catalog: cataloger configuration is done based on src
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Additionally, here is a [gist of using syft as a library](https://gist.github.com/spiffcs/3027638b7ba904d07e482a712bc00d3d) to generate a SBOM for a docker image.
|
||||||
|
|
||||||
|
|
||||||
|
### `pkg.Package` object
|
||||||
|
|
||||||
|
The `pkg.Package` object is a core data structure that represents a software package. Fields like `name` and `version` probably don't need
|
||||||
|
a detailed explanation, but some of the other fields are worth a quick overview:
|
||||||
|
|
||||||
|
- `FoundBy`: the name of the cataloger that discovered this package (e.g. `python-pip-cataloger`).
|
||||||
|
- `Locations`: these are the set of paths and layer ids that were parsed to discover this package (e.g. `python-pip-cataloger`).
|
||||||
|
- `Language`: the language of the package (e.g. `python`).
|
||||||
|
- `Type`: this is a high-level categorization of the ecosystem the package resides in. For instance, even if the package is a egg, wheel, or requirements.txt reference, it is still logically a "python" package. Not all package types align with a language (e.g. `rpm`) but it is common.
|
||||||
|
- `Metadata`: specialized data for specific location(s) parsed. We should try and raise up as much raw information that seems useful. As a rule of thumb the object here should be as flat as possible and use the raw names and values from the underlying source material parsed.
|
||||||
|
|
||||||
|
When `pkg.Package` is serialized an additional `MetadataType` is shown. This is a label that helps consumers understand the datashape of the `Metadata` field.
|
||||||
|
|
||||||
|
By convention the `MetadataType` value should follow these rules of thumb:
|
||||||
|
|
||||||
|
- Only use lowercase letters, numbers, and hyphens. Use hyphens to separate words.
|
||||||
|
- **Try to anchor the name in the ecosystem, language, or packaging tooling it belongs to**. For a package manager for a language ecosystem the language, framework or runtime should be used as a prefix. For instance `pubspec-lock` is an OK name, but `dart-pubspec-lock` is better. For an OS package manager this is not necessary (e.g. `apk-db-entry` is a good name, but `alpine-apk-db-entry` is not since `alpine` and the `a` in `apk` is redundant).
|
||||||
|
- **Be as specific as possible to what the data represents**. For instance `ruby-gem` is NOT a good `MetadataType` value, but `ruby-gemspec` is. Why? Ruby gem information can come from a gemspec file or a Gemfile.lock, which are very different. The latter name provides more context as to what to expect.
|
||||||
|
- **Should describe WHAT the data is, NOT HOW it's used**. For instance `r-description-installed-file` is NOT a good `MetadataType` value since it's trying to convey that we use the DESCRIPTION file in the R ecosystem to detect installed packages. Instead simply describe what the DESCRIPTION file is itself without context of how it's used: `r-description`.
|
||||||
|
- **Use the `lock` suffix** to distinct between manifest files that loosely describe package version requirements vs files that strongly specify one and only one version of a package ("lock" files). These should only be used with respect to package managers that have the guide and lock distinction, but would not be appropriate otherwise (e.g. `rpm` does not have a guide vs lock, so `lock` should NOT be used to describe a db entry).
|
||||||
|
- **Use the `archive` suffix to indicate a package archive** (e.g. rpm file, apk file, etc) that describes the contents of the package. For example an RPM file that was cataloged would have a `rpm-archive` metadata type (not to be confused with an RPM DB record entry which would be `rpm-db-entry`).
|
||||||
|
- **Use the `entry` suffix** to indicate information about a package that was found as a single entry within file that has multiple package entries. If the entry was found within a DB or a flat-file store for an OS package manager, you should use `db-entry`.
|
||||||
|
- **Should NOT contain the phrase `package`**, though exceptions are allowed (say if the canonical name literally has the phrase package in it).
|
||||||
|
- **Should NOT contain have a `file` suffix** unless the canonical name has the term "file", such as a `pipfile` or `gemfile`. An example of a bad name for this rule is`ruby-gemspec-file`; a better name would be `ruby-gemspec`.
|
||||||
|
- **Should NOT contain the exact filename+extensions**. For instance `pipfile.lock` shouldn't really be in the name, instead try and describe what the file is: `python-pipfile-lock` (but shouldn't this be `python-pip-lock` you might ask? No, since the `pip` package manger is not related to the `pipfile` project).
|
||||||
|
- **Should NOT contain the phrase `metadata`**, unless the canonical name has this term.
|
||||||
|
- **Should represent a single use case**. For example, trying to describe Hackage metadata with a single `HackageMetadata` struct (and thus `MetadataType`) is not allowed since it represents 3 mutually exclusive use cases: representing a `stack.yaml`, `stack.lock`, or `cabal.project` file. Instead, each of these should have their own struct types and `MetadataType` values.
|
||||||
|
|
||||||
|
There are other cases that are not covered by these rules... and that's ok! The goal is to provide a consistent naming scheme that is easy to understand and use when it's applicable. If the rules do not exactly apply in your situation then just use your best judgement (or amend these rules as needed whe new common cases come up).
|
||||||
|
|
||||||
|
What if the underlying parsed data represents multiple files? There are two approaches to this:
|
||||||
|
- use the primary file to represent all the data. For instance, though the `dpkg-cataloger` looks at multiple files to get all information about a package, it's the `status` file that gets represented.
|
||||||
|
- nest each individual file's data under the `Metadata` field. For instance, the `java-archive-cataloger` may find information from on or all of the files: `pom.xml`, `pom.properties`, and `MANIFEST.MF`. However, the metadata is simply `java-metadata' with each possibility as a nested optional field.
|
||||||
|
|
||||||
|
### Syft Catalogers
|
||||||
|
|
||||||
|
Catalogers are the way in which syft is able to identify and construct packages given a set a targeted list of files.
|
||||||
|
For example, a cataloger can ask syft for all `package-lock.json` files in order to parse and raise up javascript packages
|
||||||
|
(see [how file globs](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) and
|
||||||
|
[file parser functions](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) are used
|
||||||
|
for a quick example).
|
||||||
|
|
||||||
|
From a high level catalogers have the following properties:
|
||||||
|
|
||||||
|
- _They are independent from one another_. The java cataloger has no idea of the processes, assumptions, or results of the python cataloger, for example.
|
||||||
|
|
||||||
|
- _They do not know what source is being analyzed_. Are we analyzing a local directory? an image? if so, the squashed representation or all layers? The catalogers do not know the answers to these questions. Only that there is an interface to query for file paths and contents from an underlying "source" being scanned.
|
||||||
|
|
||||||
|
- _Packages created by the cataloger should not be mutated after they are created_. There is one exception made for adding CPEs to a package after the cataloging phase, but that will most likely be moved back into the cataloger in the future.
|
||||||
|
|
||||||
|
|
||||||
|
Cataloger names should be unique and named with the following rules of thumb in mind:
|
||||||
|
|
||||||
|
- Must end with `-cataloger`
|
||||||
|
- Use lowercase letters, numbers, and hyphens only
|
||||||
|
- Use hyphens to separate words
|
||||||
|
- Catalogers for language ecosystems should start with the language name (e.g. `python-` for a cataloger that raises up python packages)
|
||||||
|
- Distinct between when the cataloger is searching for evidence of installed packages vs declared packages. For example, there are currently two different gemspec-based catalogers, the `ruby-gemspec-cataloger` and `ruby-installed-gemspec-cataloger`, where the latter requires that the gemspec is found within a `specifications` directory (which means it was installed, not just at the root of a source repo).
|
||||||
|
|
||||||
|
#### Building a new Cataloger
|
||||||
|
|
||||||
|
Catalogers must fulfill the [`pkg.Cataloger` interface](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger.go) in order to add packages to the SBOM.
|
||||||
|
All catalogers should be added to:
|
||||||
|
- the [global list of catalogers](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/cataloger/cataloger.go#L92-L125)
|
||||||
|
- at least one source-specific list, today the two lists are [directory catalogers and image catalogers](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/cataloger/cataloger.go#L39-L89)
|
||||||
|
|
||||||
|
For reference, catalogers are [invoked within syft](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/catalog.go#L41-L100) one after the other, and can be invoked in parallel.
|
||||||
|
|
||||||
|
`generic.NewCataloger` is an abstraction syft used to make writing common components easier (see the [apkdb cataloger](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/cataloger.go) for example usage).
|
||||||
|
It takes the following information as input:
|
||||||
|
- A `catalogerName` to identify the cataloger uniquely among all other catalogers.
|
||||||
|
- Pairs of file globs as well as parser functions to parse those files. These parser functions return a slice of [`pkg.Package`](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/pkg/package.go#L19) as well as a slice of [`artifact.Relationship`](https://github.com/anchore/syft/blob/9995950c70e849f9921919faffbfcf46401f71f3/syft/artifact/relationship.go#L31) to describe how the returned packages are related. See this [the apkdb cataloger parser function](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L22-L102) as an example.
|
||||||
|
|
||||||
|
Identified packages share a common `pkg.Package` struct so be sure that when the new cataloger is constructing a new package it is using the [`Package` struct](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/package.go#L16-L31).
|
||||||
|
If you want to return more information than what is available on the `pkg.Package` struct then you can do so in the `pkg.Package.Metadata` section of the struct, which is unique for each [`pkg.Type`](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/type.go).
|
||||||
|
See [the `pkg` package](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg) for examples of the different metadata types that are supported today.
|
||||||
|
These are plugged into the `MetadataType` and `Metadata` fields in the above struct. `MetadataType` informs which type is being used. `Metadata` is an interface converted to that type.
|
||||||
|
|
||||||
|
Finally, here is an example of where the package construction is done within the apk cataloger:
|
||||||
|
- [Calling the APK package constructor from the parser function](https://github.com/anchore/syft/blob/v0.70.0/syft/pkg/cataloger/apkdb/parse_apk_db.go#L106)
|
||||||
|
- [The APK package constructor itself](https://github.com/anchore/syft/tree/v0.70.0/syft/pkg/cataloger/apkdb/package.go#L12-L27)
|
||||||
|
|
||||||
|
Interested in building a new cataloger? Checkout the [list of issues with the `new-cataloger` label](https://github.com/anchore/syft/issues?q=is%3Aopen+is%3Aissue+label%3Anew-cataloger+no%3Aassignee)!
|
||||||
|
If you have questions about implementing a cataloger feel free to file an issue or reach out to us [on slack](https://anchore.com/slack)!
|
||||||
|
|
||||||
|
|
||||||
|
#### Searching for files
|
||||||
|
|
||||||
|
All catalogers are provided an instance of the [`file.Resolver`](https://github.com/anchore/syft/blob/v0.70.0/syft/source/file_resolver.go#L8) to interface with the image and search for files. The implementations for these
|
||||||
|
abstractions leverage [`stereoscope`](https://github.com/anchore/stereoscope) in order to perform searching. Here is a
|
||||||
|
rough outline how that works:
|
||||||
|
|
||||||
|
1. a stereoscope `file.Index` is searched based on the input given (a path, glob, or MIME type). The index is relatively fast to search, but requires results to be filtered down to the files that exist in the specific layer(s) of interest. This is done automatically by the `filetree.Searcher` abstraction. This abstraction will fallback to searching directly against the raw `filetree.FileTree` if the index does not contain the file(s) of interest. Note: the `filetree.Searcher` is used by the `file.Resolver` abstraction.
|
||||||
|
2. Once the set of files are returned from the `filetree.Searcher` the results are filtered down further to return the most unique file results. For example, you may have requested for files by a glob that returns multiple results. These results are filtered down to deduplicate by real files, so if a result contains two references to the same file, say one accessed via symlink and one accessed via the real path, then the real path reference is returned and the symlink reference is filtered out. If both were accessed by symlink then the first (by lexical order) is returned. This is done automatically by the `file.Resolver` abstraction.
|
||||||
|
3. By the time results reach the `pkg.Cataloger` you are guaranteed to have a set of unique files that exist in the layer(s) of interest (relative to what the resolver supports).
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
### Levels of testing
|
### Levels of testing
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM gcr.io/distroless/static-debian11:debug AS build
|
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951 AS build
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
# needed for version check HTTPS request
|
# needed for version check HTTPS request
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM gcr.io/distroless/static-debian11:debug
|
FROM gcr.io/distroless/static-debian11:debug@sha256:a0a404776dec98be120089ae42bbdfbe48c177921d856937d124d48eb8c0b951
|
||||||
|
|
||||||
# create the /tmp dir, which is needed for image content cache
|
# create the /tmp dir, which is needed for image content cache
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
|
|||||||
@@ -1,408 +1,46 @@
|
|||||||
BIN = syft
|
OWNER = anchore
|
||||||
VERSION=$(shell git describe --dirty --always --tags)
|
PROJECT = syft
|
||||||
TEMPDIR = ./.tmp
|
|
||||||
|
|
||||||
# commands and versions
|
TOOL_DIR = .tool
|
||||||
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false
|
BINNY = $(TOOL_DIR)/binny
|
||||||
GOIMPORTS_CMD = $(TEMPDIR)/gosimports -local github.com/anchore
|
TASK = $(TOOL_DIR)/task
|
||||||
RELEASE_CMD=$(TEMPDIR)/goreleaser release --rm-dist
|
|
||||||
SNAPSHOT_CMD=$(RELEASE_CMD) --skip-publish --snapshot
|
|
||||||
|
|
||||||
# tool versions
|
.DEFAULT_GOAL := make-default
|
||||||
GOLANGCILINT_VERSION = v1.50.1
|
|
||||||
GOSIMPORTS_VERSION = v0.3.4
|
|
||||||
BOUNCER_VERSION = v0.4.0
|
|
||||||
CHRONICLE_VERSION = v0.4.2
|
|
||||||
GORELEASER_VERSION = v1.13.1
|
|
||||||
YAJSV_VERSION = v1.4.1
|
|
||||||
COSIGN_VERSION = v1.13.1
|
|
||||||
QUILL_VERSION = v0.2.0
|
|
||||||
|
|
||||||
# formatting variables
|
## Bootstrapping targets #################################
|
||||||
BOLD := $(shell tput -T linux bold)
|
|
||||||
PURPLE := $(shell tput -T linux setaf 5)
|
|
||||||
GREEN := $(shell tput -T linux setaf 2)
|
|
||||||
CYAN := $(shell tput -T linux setaf 6)
|
|
||||||
RED := $(shell tput -T linux setaf 1)
|
|
||||||
RESET := $(shell tput -T linux sgr0)
|
|
||||||
TITLE := $(BOLD)$(PURPLE)
|
|
||||||
SUCCESS := $(BOLD)$(GREEN)
|
|
||||||
|
|
||||||
# test variables
|
# note: we need to assume that binny and task have not already been installed
|
||||||
RESULTSDIR = test/results
|
$(BINNY):
|
||||||
COMPARE_DIR = ./test/compare
|
@mkdir -p $(TOOL_DIR)
|
||||||
COMPARE_TEST_IMAGE = centos:8.2.2004
|
@curl -sSfL https://raw.githubusercontent.com/$(OWNER)/binny/main/install.sh | sh -s -- -b $(TOOL_DIR)
|
||||||
COVER_REPORT = $(RESULTSDIR)/unit-coverage-details.txt
|
|
||||||
COVER_TOTAL = $(RESULTSDIR)/unit-coverage-summary.txt
|
|
||||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
|
||||||
COVERAGE_THRESHOLD := 62
|
|
||||||
|
|
||||||
# CI cache busting values; change these if you want CI to not use previous stored cache
|
# note: we need to assume that binny and task have not already been installed
|
||||||
INTEGRATION_CACHE_BUSTER="894d8ca"
|
.PHONY: task
|
||||||
CLI_CACHE_BUSTER="e5cdfd8"
|
$(TASK) task: $(BINNY)
|
||||||
BOOTSTRAP_CACHE="c7afb99ad"
|
@$(BINNY) install task -q
|
||||||
|
|
||||||
## Build variables
|
.PHONY: ci-bootstrap-go
|
||||||
DISTDIR=./dist
|
ci-bootstrap-go:
|
||||||
SNAPSHOTDIR=./snapshot
|
|
||||||
OS=$(shell uname | tr '[:upper:]' '[:lower:]')
|
|
||||||
SNAPSHOT_BIN=$(realpath $(shell pwd)/$(SNAPSHOTDIR)/$(OS)-build_$(OS)_amd64_v1/$(BIN))
|
|
||||||
|
|
||||||
## Variable assertions
|
|
||||||
ifndef TEMPDIR
|
|
||||||
$(error TEMPDIR is not set)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifndef RESULTSDIR
|
|
||||||
$(error RESULTSDIR is not set)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifndef COMPARE_DIR
|
|
||||||
$(error COMPARE_DIR is not set)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifndef DISTDIR
|
|
||||||
$(error DISTDIR is not set)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifndef SNAPSHOTDIR
|
|
||||||
$(error SNAPSHOTDIR is not set)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifndef VERSION
|
|
||||||
$(error VERSION is not set)
|
|
||||||
endif
|
|
||||||
|
|
||||||
ifndef REF_NAME
|
|
||||||
REF_NAME = $(VERSION)
|
|
||||||
endif
|
|
||||||
|
|
||||||
define title
|
|
||||||
@printf '$(TITLE)$(1)$(RESET)\n'
|
|
||||||
endef
|
|
||||||
|
|
||||||
define safe_rm_rf
|
|
||||||
bash -c 'test -z "$(1)" && false || rm -rf $(1)'
|
|
||||||
endef
|
|
||||||
|
|
||||||
define safe_rm_rf_children
|
|
||||||
bash -c 'test -z "$(1)" && false || rm -rf $(1)/*'
|
|
||||||
endef
|
|
||||||
|
|
||||||
## Default Task
|
|
||||||
.DEFAULT_GOAL:=help
|
|
||||||
|
|
||||||
## Tasks
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: clean static-analysis test ## Run all linux-based checks (linting, license check, unit, integration, and linux compare tests)
|
|
||||||
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
|
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test: unit validate-cyclonedx-schema integration benchmark compare-linux cli ## Run all tests (currently unit, integration, linux compare, and cli tests)
|
|
||||||
|
|
||||||
.PHONY: ci-bootstrap
|
|
||||||
ci-bootstrap:
|
|
||||||
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils
|
|
||||||
|
|
||||||
.PHONY:
|
|
||||||
ci-bootstrap-mac:
|
|
||||||
github_changelog_generator --version || sudo gem install github_changelog_generator
|
|
||||||
|
|
||||||
$(RESULTSDIR):
|
|
||||||
mkdir -p $(RESULTSDIR)
|
|
||||||
|
|
||||||
$(TEMPDIR):
|
|
||||||
mkdir -p $(TEMPDIR)
|
|
||||||
|
|
||||||
.PHONY: bootstrap-tools
|
|
||||||
bootstrap-tools: $(TEMPDIR)
|
|
||||||
curl -sSfL https://raw.githubusercontent.com/anchore/quill/main/install.sh | sh -s -- -b $(TEMPDIR)/ $(QUILL_VERSION)
|
|
||||||
GO111MODULE=off GOBIN=$(realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
|
|
||||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ $(GOLANGCILINT_VERSION)
|
|
||||||
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ $(BOUNCER_VERSION)
|
|
||||||
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ $(CHRONICLE_VERSION)
|
|
||||||
.github/scripts/goreleaser-install.sh -d -b $(TEMPDIR)/ $(GORELEASER_VERSION)
|
|
||||||
# the only difference between goimports and gosimports is that gosimports removes extra whitespace between import blocks (see https://github.com/golang/go/issues/20818)
|
|
||||||
GOBIN="$(realpath $(TEMPDIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION)
|
|
||||||
GOBIN="$(realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@$(YAJSV_VERSION)
|
|
||||||
GOBIN="$(realpath $(TEMPDIR))" go install github.com/sigstore/cosign/cmd/cosign@$(COSIGN_VERSION)
|
|
||||||
|
|
||||||
.PHONY: bootstrap-go
|
|
||||||
bootstrap-go:
|
|
||||||
go mod download
|
go mod download
|
||||||
|
|
||||||
.PHONY: bootstrap
|
# this is a bootstrapping catch-all, where if the target doesn't exist, we'll ensure the tools are installed and then try again
|
||||||
bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install all tooling dependencies (+ prep tooling in the ./tmp dir)
|
%:
|
||||||
$(call title,Bootstrapping dependencies)
|
make $(TASK)
|
||||||
|
$(TASK) $@
|
||||||
|
|
||||||
.PHONY: static-analysis
|
## Shim targets #################################
|
||||||
static-analysis: check-go-mod-tidy check-licenses lint
|
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: make-default
|
||||||
lint: ## Run gofmt + golangci lint checks
|
make-default: $(TASK)
|
||||||
$(call title,Running linters)
|
@# run the default task in the taskfile
|
||||||
# ensure there are no go fmt differences
|
@$(TASK)
|
||||||
@printf "files with gofmt issues: [$(shell gofmt -l -s .)]\n"
|
|
||||||
@test -z "$(shell gofmt -l -s .)"
|
|
||||||
|
|
||||||
# run all golangci-lint rules
|
# for those of us that can't seem to kick the habit of typing `make ...` lets wrap the superior `task` tool
|
||||||
$(LINTCMD)
|
TASKS := $(shell bash -c "test -f $(TASK) && $(TASK) -l | grep '^\* ' | cut -d' ' -f2 | tr -d ':' | tr '\n' ' '" ) $(shell bash -c "test -f $(TASK) && $(TASK) -l | grep 'aliases:' | cut -d ':' -f 3 | tr '\n' ' ' | tr -d ','")
|
||||||
@[ -z "$(shell $(GOIMPORTS_CMD) -d .)" ] || (echo "goimports needs to be fixed" && false)
|
|
||||||
|
|
||||||
# go tooling does not play well with certain filename characters, ensure the common cases don't result in future "go get" failures
|
.PHONY: $(TASKS)
|
||||||
$(eval MALFORMED_FILENAMES := $(shell find . | grep -e ':'))
|
$(TASKS): $(TASK)
|
||||||
@bash -c "[[ '$(MALFORMED_FILENAMES)' == '' ]] || (printf '\nfound unsupported filename characters:\n$(MALFORMED_FILENAMES)\n\n' && false)"
|
@$(TASK) $@
|
||||||
|
|
||||||
.PHONY: lint-fix
|
help: $(TASK)
|
||||||
lint-fix: ## Auto-format all source code + run golangci lint fixers
|
@$(TASK) -l
|
||||||
$(call title,Running lint fixers)
|
|
||||||
gofmt -w -s .
|
|
||||||
$(GOIMPORTS_CMD) -w .
|
|
||||||
$(LINTCMD) --fix
|
|
||||||
go mod tidy
|
|
||||||
|
|
||||||
.PHONY: check-licenses
|
|
||||||
check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
|
|
||||||
$(TEMPDIR)/bouncer check ./...
|
|
||||||
|
|
||||||
check-go-mod-tidy:
|
|
||||||
@ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
|
|
||||||
|
|
||||||
.PHONY: validate-cyclonedx-schema
|
|
||||||
validate-cyclonedx-schema:
|
|
||||||
cd schema/cyclonedx && make
|
|
||||||
|
|
||||||
.PHONY: unit
|
|
||||||
unit: $(RESULTSDIR) fixtures ## Run unit tests (with coverage)
|
|
||||||
$(call title,Running unit tests)
|
|
||||||
go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/test)
|
|
||||||
@go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL)
|
|
||||||
@echo "Coverage: $$(cat $(COVER_TOTAL))"
|
|
||||||
@if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi
|
|
||||||
|
|
||||||
.PHONY: benchmark
|
|
||||||
benchmark: $(RESULTSDIR) ## Run benchmark tests and compare against the baseline (if available)
|
|
||||||
$(call title,Running benchmark tests)
|
|
||||||
go test -p 1 -run=^Benchmark -bench=. -count=5 -benchmem ./... | tee $(RESULTSDIR)/benchmark-$(REF_NAME).txt
|
|
||||||
(test -s $(RESULTSDIR)/benchmark-main.txt && \
|
|
||||||
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-main.txt $(RESULTSDIR)/benchmark-$(REF_NAME).txt || \
|
|
||||||
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-$(REF_NAME).txt) \
|
|
||||||
| tee $(RESULTSDIR)/benchstat.txt
|
|
||||||
|
|
||||||
.PHONY: show-benchstat
|
|
||||||
show-benchstat:
|
|
||||||
@cat $(RESULTSDIR)/benchstat.txt
|
|
||||||
|
|
||||||
# note: this is used by CI to determine if the install test fixture cache (docker image tars) should be busted
|
|
||||||
install-fingerprint:
|
|
||||||
cd test/install && \
|
|
||||||
make cache.fingerprint
|
|
||||||
|
|
||||||
install-test: $(SNAPSHOTDIR)
|
|
||||||
cd test/install && \
|
|
||||||
make
|
|
||||||
|
|
||||||
install-test-cache-save: $(SNAPSHOTDIR)
|
|
||||||
cd test/install && \
|
|
||||||
make save
|
|
||||||
|
|
||||||
install-test-cache-load: $(SNAPSHOTDIR)
|
|
||||||
cd test/install && \
|
|
||||||
make load
|
|
||||||
|
|
||||||
install-test-ci-mac: $(SNAPSHOTDIR)
|
|
||||||
cd test/install && \
|
|
||||||
make ci-test-mac
|
|
||||||
|
|
||||||
.PHONY: integration
|
|
||||||
integration: ## Run integration tests
|
|
||||||
$(call title,Running integration tests)
|
|
||||||
go test -v ./test/integration
|
|
||||||
|
|
||||||
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
|
|
||||||
integration-fingerprint:
|
|
||||||
$(call title,Integration test fixture fingerprint)
|
|
||||||
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee test/integration/test-fixtures/cache.fingerprint && echo "$(INTEGRATION_CACHE_BUSTER)" >> test/integration/test-fixtures/cache.fingerprint
|
|
||||||
|
|
||||||
.PHONY: java-packages-fingerprint
|
|
||||||
java-packages-fingerprint:
|
|
||||||
$(call title,Java test fixture fingerprint)
|
|
||||||
cd syft/pkg/cataloger/java/test-fixtures/java-builds && \
|
|
||||||
make packages.fingerprint
|
|
||||||
|
|
||||||
.PHONY: go-binaries-fingerprint
|
|
||||||
go-binaries-fingerprint:
|
|
||||||
$(call title,Go binaries test fixture fingerprint)
|
|
||||||
cd syft/pkg/cataloger/golang/test-fixtures/archs && \
|
|
||||||
make binaries.fingerprint
|
|
||||||
|
|
||||||
.PHONY: rpm-binaries-fingerprint
|
|
||||||
rpm-binaries-fingerprint:
|
|
||||||
$(call title,RPM binary test fixture fingerprint)
|
|
||||||
cd syft/pkg/cataloger/rpm/test-fixtures && \
|
|
||||||
make rpms.fingerprint
|
|
||||||
|
|
||||||
.PHONY: fixtures
|
|
||||||
fixtures:
|
|
||||||
$(call title,Generating test fixtures)
|
|
||||||
cd syft/pkg/cataloger/java/test-fixtures/java-builds && make
|
|
||||||
cd syft/pkg/cataloger/rpm/test-fixtures && make
|
|
||||||
|
|
||||||
.PHONY: generate-json-schema
|
|
||||||
generate-json-schema: ## Generate a new json schema
|
|
||||||
cd schema/json && go run generate.go
|
|
||||||
|
|
||||||
.PHONY: generate-license-list
|
|
||||||
generate-license-list: ## Generate an updated spdx license list
|
|
||||||
go generate ./internal/spdxlicense/...
|
|
||||||
gofmt -s -w ./internal/spdxlicense
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
|
|
||||||
|
|
||||||
$(SNAPSHOTDIR): ## Build snapshot release binaries and packages
|
|
||||||
$(call title,Building snapshot artifacts)
|
|
||||||
|
|
||||||
# create a config with the dist dir overridden
|
|
||||||
echo "dist: $(SNAPSHOTDIR)" > $(TEMPDIR)/goreleaser.yaml
|
|
||||||
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
|
|
||||||
|
|
||||||
# build release snapshots
|
|
||||||
bash -c "SKIP_SIGNING=true $(SNAPSHOT_CMD) --skip-sign --config $(TEMPDIR)/goreleaser.yaml"
|
|
||||||
|
|
||||||
.PHONY: snapshot-with-signing
|
|
||||||
snapshot-with-signing: ## Build snapshot release binaries and packages (with dummy signing)
|
|
||||||
$(call title,Building snapshot artifacts (+ signing))
|
|
||||||
|
|
||||||
# create a config with the dist dir overridden
|
|
||||||
echo "dist: $(SNAPSHOTDIR)" > $(TEMPDIR)/goreleaser.yaml
|
|
||||||
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
|
|
||||||
|
|
||||||
# build release snapshots
|
|
||||||
bash -c "\
|
|
||||||
$(SNAPSHOT_CMD) \
|
|
||||||
--config $(TEMPDIR)/goreleaser.yaml \
|
|
||||||
|| (cat /tmp/quill-*.log && false)"
|
|
||||||
|
|
||||||
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
|
|
||||||
.PHONY: compare-mac
|
|
||||||
compare-mac: $(RESULTSDIR) $(SNAPSHOTDIR) ## Run compare tests on build snapshot binaries and packages (Mac)
|
|
||||||
$(call title,Running compare test: Run on Mac)
|
|
||||||
$(COMPARE_DIR)/mac.sh \
|
|
||||||
$(SNAPSHOTDIR) \
|
|
||||||
$(COMPARE_DIR) \
|
|
||||||
$(COMPARE_TEST_IMAGE) \
|
|
||||||
$(RESULTSDIR)
|
|
||||||
|
|
||||||
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
|
|
||||||
.PHONY: compare-linux
|
|
||||||
compare-linux: compare-test-deb-package-install compare-test-rpm-package-install ## Run compare tests on build snapshot binaries and packages (Linux)
|
|
||||||
|
|
||||||
.PHONY: compare-test-deb-package-install
|
|
||||||
compare-test-deb-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
|
|
||||||
$(call title,Running compare test: DEB install)
|
|
||||||
$(COMPARE_DIR)/deb.sh \
|
|
||||||
$(SNAPSHOTDIR) \
|
|
||||||
$(COMPARE_DIR) \
|
|
||||||
$(COMPARE_TEST_IMAGE) \
|
|
||||||
$(RESULTSDIR)
|
|
||||||
|
|
||||||
.PHONY: compare-test-rpm-package-install
|
|
||||||
compare-test-rpm-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
|
|
||||||
$(call title,Running compare test: RPM install)
|
|
||||||
$(COMPARE_DIR)/rpm.sh \
|
|
||||||
$(SNAPSHOTDIR) \
|
|
||||||
$(COMPARE_DIR) \
|
|
||||||
$(COMPARE_TEST_IMAGE) \
|
|
||||||
$(RESULTSDIR)
|
|
||||||
|
|
||||||
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
|
|
||||||
cli-fingerprint:
|
|
||||||
$(call title,CLI test fixture fingerprint)
|
|
||||||
find test/cli/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/cli/test-fixtures/cache.fingerprint && echo "$(CLI_CACHE_BUSTER)" >> test/cli/test-fixtures/cache.fingerprint
|
|
||||||
|
|
||||||
.PHONY: cli
|
|
||||||
cli: $(SNAPSHOTDIR) ## Run CLI tests
|
|
||||||
chmod 755 "$(SNAPSHOT_BIN)"
|
|
||||||
$(SNAPSHOT_BIN) version
|
|
||||||
SYFT_BINARY_LOCATION='$(SNAPSHOT_BIN)' \
|
|
||||||
go test -count=1 -timeout=15m -v ./test/cli
|
|
||||||
|
|
||||||
.PHONY: changelog
|
|
||||||
changelog: clean-changelog CHANGELOG.md
|
|
||||||
@docker run -it --rm \
|
|
||||||
-v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \
|
|
||||||
rawkode/mdv \
|
|
||||||
-t 748.5989 \
|
|
||||||
/CHANGELOG.md
|
|
||||||
|
|
||||||
CHANGELOG.md:
|
|
||||||
$(TEMPDIR)/chronicle -vv > CHANGELOG.md
|
|
||||||
|
|
||||||
.PHONY: release
|
|
||||||
release: clean-dist CHANGELOG.md
|
|
||||||
$(call title,Publishing release artifacts)
|
|
||||||
|
|
||||||
# create a config with the dist dir overridden
|
|
||||||
echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml
|
|
||||||
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
|
|
||||||
|
|
||||||
# note: notarization cannot be done in parallel, thus --parallelism 1
|
|
||||||
bash -c "\
|
|
||||||
$(RELEASE_CMD) \
|
|
||||||
--config $(TEMPDIR)/goreleaser.yaml \
|
|
||||||
--parallelism 1 \
|
|
||||||
--release-notes <(cat CHANGELOG.md) \
|
|
||||||
|| (cat /tmp/quill-*.log && false)"
|
|
||||||
|
|
||||||
# TODO: turn this into a post-release hook
|
|
||||||
# upload the version file that supports the application version update check (excluding pre-releases)
|
|
||||||
.github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)"
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean: clean-dist clean-snapshot clean-test-image-cache ## Remove previous builds, result reports, and test cache
|
|
||||||
$(call safe_rm_rf_children,$(RESULTSDIR))
|
|
||||||
|
|
||||||
.PHONY: clean-snapshot
|
|
||||||
clean-snapshot:
|
|
||||||
$(call safe_rm_rf,$(SNAPSHOTDIR))
|
|
||||||
rm -f $(TEMPDIR)/goreleaser.yaml
|
|
||||||
|
|
||||||
.PHONY: clean-dist
|
|
||||||
clean-dist: clean-changelog
|
|
||||||
$(call safe_rm_rf,$(DISTDIR))
|
|
||||||
rm -f $(TEMPDIR)/goreleaser.yaml
|
|
||||||
|
|
||||||
.PHONY: clean-changelog
|
|
||||||
clean-changelog:
|
|
||||||
rm -f CHANGELOG.md
|
|
||||||
|
|
||||||
clean-test-image-cache: clean-test-image-tar-cache clean-test-image-docker-cache ## Clean test image cache
|
|
||||||
|
|
||||||
.PHONY: clear-test-image-tar-cache
|
|
||||||
clean-test-image-tar-cache:
|
|
||||||
## Delete all test cache (built docker image tars)
|
|
||||||
find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" -delete
|
|
||||||
|
|
||||||
.PHONY: clear-test-image-docker-cache
|
|
||||||
clean-test-image-docker-cache:
|
|
||||||
## Purge all test docker images
|
|
||||||
docker images --format '{{.ID}} {{.Repository}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs -r docker rmi --force
|
|
||||||
|
|
||||||
.PHONY: show-test-image-cache
|
|
||||||
show-test-image-cache: ## Show all docker and image tar cache
|
|
||||||
$(call title,Docker daemon cache)
|
|
||||||
@docker images --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep stereoscope-fixture- | sort
|
|
||||||
|
|
||||||
$(call title,Tar cache)
|
|
||||||
@find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" | sort
|
|
||||||
|
|
||||||
.PHONY: show-test-snapshots
|
|
||||||
show-test-snapshots: ## Show all test snapshots
|
|
||||||
$(call title,Test snapshots)
|
|
||||||
@find . -type f -wholename "**/test-fixtures/snapshot/*" | sort
|
|
||||||
|
|
||||||
.PHONY: help
|
|
||||||
help: ## Display this help
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
|
|
||||||
|
|||||||
@@ -38,17 +38,22 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
|
|||||||
- Debian (dpkg)
|
- Debian (dpkg)
|
||||||
- Dotnet (deps.json)
|
- Dotnet (deps.json)
|
||||||
- Objective-C (cocoapods)
|
- Objective-C (cocoapods)
|
||||||
|
- Elixir (mix)
|
||||||
|
- Erlang (rebar3)
|
||||||
- Go (go.mod, Go binaries)
|
- Go (go.mod, Go binaries)
|
||||||
- Haskell (cabal, stack)
|
- Haskell (cabal, stack)
|
||||||
- Java (jar, ear, war, par, sar)
|
- Java (jar, ear, war, par, sar, nar, native-image)
|
||||||
- JavaScript (npm, yarn)
|
- JavaScript (npm, yarn)
|
||||||
- Jenkins Plugins (jpi, hpi)
|
- Jenkins Plugins (jpi, hpi)
|
||||||
|
- Linux kernel archives (vmlinz)
|
||||||
|
- Linux kernel modules (ko)
|
||||||
|
- Nix (outputs in /nix/store)
|
||||||
- PHP (composer)
|
- PHP (composer)
|
||||||
- Python (wheel, egg, poetry, requirements.txt)
|
- Python (wheel, egg, poetry, requirements.txt)
|
||||||
- Red Hat (rpm)
|
- Red Hat (rpm)
|
||||||
- Ruby (gem)
|
- Ruby (gem)
|
||||||
- Rust (cargo.lock)
|
- Rust (cargo.lock)
|
||||||
- Swift (cocoapods)
|
- Swift (cocoapods, swift-package-manager)
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -73,15 +78,20 @@ The chocolatey distribution of syft is community maintained and not distributed
|
|||||||
choco install syft -y
|
choco install syft -y
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Scoop
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scoop install syft
|
||||||
|
```
|
||||||
|
|
||||||
### Homebrew
|
### Homebrew
|
||||||
```bash
|
```bash
|
||||||
brew tap anchore/syft
|
|
||||||
brew install syft
|
brew install syft
|
||||||
```
|
```
|
||||||
|
|
||||||
### Nix
|
### Nix
|
||||||
|
|
||||||
**Note**: Nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/admin/syft/default.nix). Syft is currently only in the [unstable channel](https://nixos.wiki/wiki/Nix_channels#The_official_channels) awaiting the `22.05` release
|
**Note**: Nix packaging of Syft is [community maintained](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/admin/syft/default.nix). Syft is available in the [stable channel](https://nixos.wiki/wiki/Nix_channels#The_official_channels) since NixOS `22.05`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix-env -i syft
|
nix-env -i syft
|
||||||
@@ -99,23 +109,21 @@ nix-shell -p syft
|
|||||||
|
|
||||||
To generate an SBOM for a container image:
|
To generate an SBOM for a container image:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
syft <image>
|
syft <image>
|
||||||
```
|
```
|
||||||
|
|
||||||
The above output includes only software that is visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
|
The above output includes only software that is visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
syft <image> --scope all-layers
|
syft <image> --scope all-layers
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Supported sources
|
||||||
|
|
||||||
|
Syft can generate an SBOM from a variety of sources:
|
||||||
|
|
||||||
## Supported sources
|
```bash
|
||||||
|
|
||||||
Syft can generate a SBOM from a variety of sources:
|
|
||||||
|
|
||||||
```
|
|
||||||
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
|
||||||
syft path/to/image.tar
|
syft path/to/image.tar
|
||||||
|
|
||||||
@@ -131,6 +139,7 @@ Sources can be explicitly provided with a scheme:
|
|||||||
```
|
```
|
||||||
docker:yourrepo/yourimage:tag use images from the Docker daemon
|
docker:yourrepo/yourimage:tag use images from the Docker daemon
|
||||||
podman:yourrepo/yourimage:tag use images from the Podman daemon
|
podman:yourrepo/yourimage:tag use images from the Podman daemon
|
||||||
|
containerd:yourrepo/yourimage:tag use images from the Containerd daemon
|
||||||
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||||
oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||||
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||||
@@ -140,46 +149,120 @@ file:path/to/yourproject/file read directly from a path on disk (any
|
|||||||
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Default Cataloger Configuration by scan type
|
If an image source is not provided and cannot be detected from the given reference it is assumed the image should be pulled from the Docker daemon.
|
||||||
|
If docker is not present, then the Podman daemon is attempted next, followed by reaching out directly to the image registry last.
|
||||||
|
|
||||||
##### Image Scanning:
|
|
||||||
- alpmdb
|
|
||||||
- rpmdb
|
|
||||||
- dpkgdb
|
|
||||||
- apkdb
|
|
||||||
- portage
|
|
||||||
- ruby-gemspec
|
|
||||||
- python-package
|
|
||||||
- php-composer-installed Cataloger
|
|
||||||
- javascript-package
|
|
||||||
- java
|
|
||||||
- go-module-binary
|
|
||||||
- dotnet-deps
|
|
||||||
|
|
||||||
##### Directory Scanning:
|
This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](https://github.com/anchore/syft#configuration) for more details).
|
||||||
- alpmdb
|
|
||||||
- apkdb
|
|
||||||
- dpkgdb
|
### File selection
|
||||||
- portage
|
|
||||||
- rpmdb
|
By default, Syft will catalog file details and digests for files that are owned by discovered packages. You can change this behavior by using the `SYFT_FILE_METADATA_SELECTION` environment variable or the `file.metadata.selection` configuration option. The options are:
|
||||||
- ruby-gemfile
|
|
||||||
- python-index
|
- `all`: capture all files from the search space
|
||||||
- python-package
|
- `owned-by-package`: capture only files owned by packages (default)
|
||||||
- php-composer-lock
|
- `none`: disable capturing any file information
|
||||||
- javascript-lock
|
|
||||||
- java
|
|
||||||
- java-pom
|
### Package cataloger selection
|
||||||
- go-module-binary
|
|
||||||
- go-mod-file
|
#### Concepts
|
||||||
- rust-cargo-lock
|
|
||||||
- dartlang-lock
|
> [!IMPORTANT]
|
||||||
- dotnet-deps
|
> Syft uses a different set of catalogers by default when scanning files directly than it does when scanning images
|
||||||
- cocoapods
|
|
||||||
- conan
|
The catalogers for an image scan assumes that package installation steps have already been completed. For example, Syft will identify Python packages that have egg or wheel metadata files under a `site-packages` directory, since this is how the canonical tooling `pip` installs python packages.
|
||||||
- hackage
|
|
||||||
|
The catalogers for a directory scan will look for installed software as well as declared dependencies that are not necessarily installed. For example, dependencies listed in a Python `requirements.txt`.
|
||||||
|
|
||||||
|
This default set of catalogers being dynamic is critical as this allows Syft to be used in a variety of contexts while still generating accurate SBOMs.
|
||||||
|
Overriding the set of default catalogers is not recommended for most purposes, however, is possible if needed.
|
||||||
|
|
||||||
|
Catalogers can be referenced in two different ways:
|
||||||
|
- *by name*: the exact cataloger name (e.g. `java-pom-cataloger` or `java-archive-cataloger`)
|
||||||
|
- *by tag*: a tag that is associated with a cataloger (e.g. `java`)
|
||||||
|
|
||||||
|
Syft can take lists of references on the CLI or in the application configuration to define which catalogers to use.
|
||||||
|
|
||||||
|
You can **set** the list of catalogers explicitly to use with the `--override-default-catalogers` CLI flag, accepting a comma-separated list of cataloger names or tags.
|
||||||
|
|
||||||
|
You can also **add** to, **remove** from, or **sub-select** catalogers to use within the default set of catalogers by using the `--select-catalogers` CLI flag.
|
||||||
|
- To **sub-select** catalogers simply provide a tag (e.g. `--select-catalogers TAG`). Catalogers will always be selected from the default set of catalogers (e.g. `--select-catalogers java,go` will select all the `java` catalogers in the default set and all the `go` catalogers in the default set).
|
||||||
|
- To **add** a cataloger prefix the cataloger name with `+` (e.g. `--select-catalogers +NAME`). Added catalogers will _always be added_ regardless of removals, filtering, or other defaults.
|
||||||
|
- To **remove** a cataloger prefix the cataloger name or tag with `-` (e.g. `--select-catalogers -NAME_OR_TAG`). Catalogers are removed from the set of default catalogers after processing any sub-selections.
|
||||||
|
|
||||||
|
These rules and the dynamic default cataloger sets approximates to the following logic:
|
||||||
|
|
||||||
|
```
|
||||||
|
image_catalogers = all_catalogers AND catalogers_tagged("image")
|
||||||
|
|
||||||
|
directory_catalogers = all_catalogers AND catalogers_tagged("directory")
|
||||||
|
|
||||||
|
default_catalogers = image_catalogers OR directory_catalogers
|
||||||
|
|
||||||
|
sub_selected_catalogers = default_catalogers INTERSECT catalogers_tagged(TAG) [ UNION sub_selected_catalogers ... ]
|
||||||
|
|
||||||
|
base_catalogers = default_catalogers OR sub_selected_catalogers
|
||||||
|
|
||||||
|
final_set = (base_catalogers SUBTRACT removed_catalogers) UNION added_catalogers
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
Only scan for python related packages with catalogers appropriate for the source type (image or directory):
|
||||||
|
```bash
|
||||||
|
syft <some container image> --select-catalogers "python"
|
||||||
|
# results in the following catalogers being used:
|
||||||
|
# - python-installed-package-cataloger
|
||||||
|
```
|
||||||
|
|
||||||
|
Same command, but the set of catalogers changes based on what is being analyzed (in this case a directory):
|
||||||
|
```bash
|
||||||
|
syft <a directory> --select-catalogers "python"
|
||||||
|
# results in the following catalogers being used:
|
||||||
|
# - python-installed-package-cataloger
|
||||||
|
# - python-package-cataloger
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the default set of catalogers and add a cataloger to the set:
|
||||||
|
```bash
|
||||||
|
syft ... --catalogers "+sbom-cataloger"
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the default set of catalogers but remove any catalogers that deal with RPMs:
|
||||||
|
```bash
|
||||||
|
syft ... --catalogers "-rpm"
|
||||||
|
```
|
||||||
|
|
||||||
|
Only scan with catalogers that:
|
||||||
|
- are tagged with "go"
|
||||||
|
- always use the sbom-cataloger
|
||||||
|
- are appropriate for the source type (image or directory)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
syft <some container image> --select-catalogers "go,+sbom-cataloger"
|
||||||
|
# results in the following catalogers being used:
|
||||||
|
# - go-module-binary-cataloger
|
||||||
|
# - sbom-cataloger
|
||||||
|
```
|
||||||
|
|
||||||
|
Scan with all catalogers that deal with binary analysis, regardless of the source type:
|
||||||
|
```bash
|
||||||
|
syft ... --override-default-catalogers "binary"
|
||||||
|
# results in the following catalogers being used:
|
||||||
|
# - binary-cataloger
|
||||||
|
# - cargo-auditable-binary-cataloger
|
||||||
|
# - dotnet-portable-executable-cataloger
|
||||||
|
# - go-module-binary-cataloger
|
||||||
|
```
|
||||||
|
|
||||||
|
Only scan with the specific `go-module-binary-cataloger` and `go-module-file-cataloger` catalogers:
|
||||||
|
```bash
|
||||||
|
syft ... --override-default-catalogers "go-module-binary-cataloger,go-module-file-cataloger"
|
||||||
|
```
|
||||||
|
|
||||||
#### Non Default:
|
|
||||||
- cargo-auditable-binary
|
|
||||||
|
|
||||||
### Excluding file paths
|
### Excluding file paths
|
||||||
|
|
||||||
@@ -208,14 +291,16 @@ syft <image> -o <format>
|
|||||||
```
|
```
|
||||||
|
|
||||||
Where the `formats` available are:
|
Where the `formats` available are:
|
||||||
- `json`: Use this to get as much information out of Syft as possible!
|
- `syft-json`: Use this to get as much information out of Syft as possible!
|
||||||
- `text`: A row-oriented, human-and-machine-friendly output.
|
- `syft-text`: A row-oriented, human-and-machine-friendly output.
|
||||||
- `cyclonedx-xml`: A XML report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
|
- `cyclonedx-xml`: A XML report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
|
||||||
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
|
- `cyclonedx-json`: A JSON report conforming to the [CycloneDX 1.4 specification](https://cyclonedx.org/specification/overview/).
|
||||||
- `spdx-tag-value`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
|
- `spdx-tag-value`: A tag-value formatted report conforming to the [SPDX 2.3 specification](https://spdx.github.io/spdx-spec/v2.3/).
|
||||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
- `spdx-tag-value@2.2`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/v2.2.2/).
|
||||||
- `github`: A JSON report conforming to GitHub's dependency snapshot format.
|
- `spdx-json`: A JSON report conforming to the [SPDX 2.3 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.3/schemas/spdx-schema.json).
|
||||||
- `table`: A columnar summary (default).
|
- `spdx-json@2.2`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||||
|
- `github-json`: A JSON report conforming to GitHub's dependency snapshot format.
|
||||||
|
- `syft-table`: A columnar summary (default).
|
||||||
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
|
- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below.
|
||||||
|
|
||||||
## Using templates
|
## Using templates
|
||||||
@@ -228,7 +313,7 @@ Syft lets you define custom output formats, using [Go templates](https://pkg.go.
|
|||||||
|
|
||||||
- Specify the path to the template file (`-t ./path/to/custom.template`).
|
- Specify the path to the template file (`-t ./path/to/custom.template`).
|
||||||
|
|
||||||
- Syft's template processing uses the same data models as the `json` output format — so if you're wondering what data is available as you author a template, you can use the output from `syft <image> -o json` as a reference.
|
- Syft's template processing uses the same data models as the `syft-json` output format — so if you're wondering what data is available as you author a template, you can use the output from `syft <image> -o syft-json` as a reference.
|
||||||
|
|
||||||
**Example:** You could make Syft output data in CSV format by writing a Go template that renders CSV data and then running `syft <image> -o template -t ~/path/to/csv.tmpl`.
|
**Example:** You could make Syft output data in CSV format by writing a Go template that renders CSV data and then running `syft <image> -o template -t ~/path/to/csv.tmpl`.
|
||||||
|
|
||||||
@@ -251,13 +336,15 @@ Which would produce output like:
|
|||||||
|
|
||||||
Syft also includes a vast array of utility templating functions from [sprig](http://masterminds.github.io/sprig/) apart from the default Golang [text/template](https://pkg.go.dev/text/template#hdr-Functions) to allow users to customize the output format.
|
Syft also includes a vast array of utility templating functions from [sprig](http://masterminds.github.io/sprig/) apart from the default Golang [text/template](https://pkg.go.dev/text/template#hdr-Functions) to allow users to customize the output format.
|
||||||
|
|
||||||
|
Lastly, Syft has custom templating functions defined in `./syft/format/template/encoder.go` to help parse the passed-in JSON structs.
|
||||||
|
|
||||||
## Multiple outputs
|
## Multiple outputs
|
||||||
|
|
||||||
Syft can also output _multiple_ files in differing formats by appending
|
Syft can also output _multiple_ files in differing formats by appending
|
||||||
`=<file>` to the option, for example to output Syft JSON and SPDX JSON:
|
`=<file>` to the option, for example to output Syft JSON and SPDX JSON:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
syft <image> -o json=sbom.syft.json -o spdx-json=sbom.spdx.json
|
syft <image> -o syft-json=sbom.syft.json -o spdx-json=sbom.spdx.json
|
||||||
```
|
```
|
||||||
|
|
||||||
## Private Registry Authentication
|
## Private Registry Authentication
|
||||||
@@ -350,11 +437,11 @@ syft convert <ORIGINAL-SBOM-FILE> -o <NEW-SBOM-FORMAT>[=<NEW-SBOM-FILE>]
|
|||||||
This feature is experimental and data might be lost when converting formats. Packages are the main SBOM component easily transferable across formats, whereas files and relationships, as well as other information Syft doesn't support, are more likely to be lost.
|
This feature is experimental and data might be lost when converting formats. Packages are the main SBOM component easily transferable across formats, whereas files and relationships, as well as other information Syft doesn't support, are more likely to be lost.
|
||||||
|
|
||||||
We support formats with wide community usage AND good encode/decode support by Syft. The supported formats are:
|
We support formats with wide community usage AND good encode/decode support by Syft. The supported formats are:
|
||||||
- Syft JSON
|
- Syft JSON (```-o syft-json```)
|
||||||
- SPDX 2.2 JSON
|
- SPDX 2.2 JSON (```-o spdx-json```)
|
||||||
- SPDX 2.2 tag-value
|
- SPDX 2.2 tag-value (```-o spdx-tag-value```)
|
||||||
- CycloneDX 1.4 JSON
|
- CycloneDX 1.4 JSON (```-o cyclonedx-json```)
|
||||||
- CycloneDX 1.4 XML
|
- CycloneDX 1.4 XML (```-o cyclonedx-xml```)
|
||||||
|
|
||||||
Conversion example:
|
Conversion example:
|
||||||
```sh
|
```sh
|
||||||
@@ -366,6 +453,8 @@ syft convert sbom.syft.json -o cyclonedx-json=sbom.cdx.json # convert it to Cyc
|
|||||||
### Keyless support
|
### Keyless support
|
||||||
Syft supports generating attestations using cosign's [keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) signatures.
|
Syft supports generating attestations using cosign's [keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) signatures.
|
||||||
|
|
||||||
|
Note: users need to have >= v1.12.0 of cosign installed for this command to function
|
||||||
|
|
||||||
To use this feature with a format like CycloneDX json simply run:
|
To use this feature with a format like CycloneDX json simply run:
|
||||||
```
|
```
|
||||||
syft attest --output cyclonedx-json <IMAGE WITH OCI WRITE ACCESS>
|
syft attest --output cyclonedx-json <IMAGE WITH OCI WRITE ACCESS>
|
||||||
@@ -388,7 +477,7 @@ Certificate subject: test.email@testdomain.com
|
|||||||
Certificate issuer URL: https://accounts.google.com
|
Certificate issuer URL: https://accounts.google.com
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Local private key support
|
### Local private key support
|
||||||
|
|
||||||
To generate an SBOM attestation for a container image using a local private key:
|
To generate an SBOM attestation for a container image using a local private key:
|
||||||
```
|
```
|
||||||
@@ -412,63 +501,166 @@ Configuration search paths:
|
|||||||
Configuration options (example values are the default):
|
Configuration options (example values are the default):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# the output format(s) of the SBOM report (options: table, text, json, spdx, ...)
|
# the output format(s) of the SBOM report (options: syft-table, syft-text, syft-json, spdx-json, ...)
|
||||||
# same as -o, --output, and SYFT_OUTPUT env var
|
# same as -o, --output, and SYFT_OUTPUT env var
|
||||||
# to specify multiple output files in differing formats, use a list:
|
# to specify multiple output files in differing formats, use a list:
|
||||||
# output:
|
# output:
|
||||||
# - "json=<syft-json-output-file>"
|
# - "syft-json=<syft-json-output-file>"
|
||||||
# - "spdx-json=<spdx-json-output-file>"
|
# - "spdx-json=<spdx-json-output-file>"
|
||||||
output: "table"
|
output: "syft-table"
|
||||||
|
|
||||||
# suppress all output (except for the SBOM report)
|
# suppress all output (except for the SBOM report)
|
||||||
# same as -q ; SYFT_QUIET env var
|
# same as -q ; SYFT_QUIET env var
|
||||||
quiet: false
|
quiet: false
|
||||||
|
|
||||||
# same as --file; write output report to a file (default is to write to stdout)
|
|
||||||
file: ""
|
|
||||||
|
|
||||||
# enable/disable checking for application updates on startup
|
# enable/disable checking for application updates on startup
|
||||||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||||
check-for-app-update: true
|
check-for-app-update: true
|
||||||
|
|
||||||
# a list of globs to exclude from scanning. same as --exclude ; for example:
|
# maximum number of workers used to process the list of package catalogers in parallel
|
||||||
|
parallelism: 1
|
||||||
|
|
||||||
|
# a list of globs to exclude from scanning, for example:
|
||||||
# exclude:
|
# exclude:
|
||||||
# - "/etc/**"
|
# - "/etc/**"
|
||||||
# - "./out/**/*.json"
|
# - "./out/**/*.json"
|
||||||
|
# same as --exclude ; SYFT_EXCLUDE env var
|
||||||
exclude: []
|
exclude: []
|
||||||
|
|
||||||
|
# allows users to exclude synthetic binary packages from the sbom
|
||||||
|
# these packages are removed if an overlap with a non-synthetic package is found
|
||||||
|
exclude-binary-overlap-by-ownership: true
|
||||||
|
|
||||||
# os and/or architecture to use when referencing container images (e.g. "windows/armv6" or "arm64")
|
# os and/or architecture to use when referencing container images (e.g. "windows/armv6" or "arm64")
|
||||||
# same as --platform; SYFT_PLATFORM env var
|
# same as --platform; SYFT_PLATFORM env var
|
||||||
platform: ""
|
platform: ""
|
||||||
|
|
||||||
|
# the search space to look for file and package data (options: all-layers, squashed)
|
||||||
|
# SYFT_SCOPE env var
|
||||||
|
scope: "squashed"
|
||||||
|
|
||||||
# set the list of package catalogers to use when generating the SBOM
|
# set the list of package catalogers to use when generating the SBOM
|
||||||
# default = empty (cataloger set determined automatically by the source type [image or file/directory])
|
# default = empty (cataloger set determined automatically by the source type [image or file/directory])
|
||||||
# catalogers:
|
# catalogers:
|
||||||
# - ruby-gemfile
|
# - alpm-db-cataloger
|
||||||
# - ruby-gemspec
|
# - apkdb-cataloger
|
||||||
# - python-index
|
# - binary-cataloger
|
||||||
# - python-package
|
# - cargo-auditable-binary-cataloger
|
||||||
# - javascript-lock
|
# - cocoapods-cataloger
|
||||||
# - javascript-package
|
# - conan-cataloger
|
||||||
# - php-composer-installed
|
# - dartlang-lock-cataloger
|
||||||
# - php-composer-lock
|
# - dotnet-deps-cataloger
|
||||||
# - alpmdb
|
# - dotnet-portable-executable-cataloger
|
||||||
# - dpkgdb
|
# - dpkg-db-cataloger
|
||||||
# - rpmdb
|
# - elixir-mix-lock-cataloger
|
||||||
# - java
|
# - erlang-rebar-lock-cataloger
|
||||||
# - apkdb
|
# - go-module-file-cataloger
|
||||||
# - go-module-binary
|
# - go-module-binary-cataloger
|
||||||
# - go-mod-file
|
# - graalvm-native-image-cataloger
|
||||||
# - dartlang-lock
|
# - haskell-cataloger
|
||||||
# - rust
|
# - java-cataloger
|
||||||
# - dotnet-deps
|
# - java-gradle-lockfile-cataloger
|
||||||
# rust-audit-binary scans Rust binaries built with https://github.com/Shnatsel/rust-audit
|
# - java-pom-cataloger
|
||||||
# - rust-audit-binary
|
# - javascript-lock-cataloger
|
||||||
|
# - javascript-package-cataloger
|
||||||
|
# - linux-kernel-cataloger
|
||||||
|
# - nix-store-cataloger
|
||||||
|
# - php-composer-installed-cataloger
|
||||||
|
# - php-composer-lock-cataloger
|
||||||
|
# - portage-cataloger
|
||||||
|
# - python-package-cataloger
|
||||||
|
# - python-installed-package-cataloger
|
||||||
|
# - rpm-db-cataloger
|
||||||
|
# - rpm-archive-cataloger
|
||||||
|
# - ruby-gemfile-cataloger
|
||||||
|
# - ruby-installed-gemspec-cataloger
|
||||||
|
# - rust-cargo-lock-cataloger
|
||||||
|
# - sbom-cataloger
|
||||||
|
# - spm-cataloger
|
||||||
catalogers:
|
catalogers:
|
||||||
|
|
||||||
|
# all format configuration
|
||||||
|
format:
|
||||||
|
|
||||||
|
# default value for all formats that support the "pretty" option (default is unset)
|
||||||
|
pretty:
|
||||||
|
|
||||||
|
# all syft-json format options
|
||||||
|
json:
|
||||||
|
|
||||||
|
# include space indention and newlines (inherits default value from 'format.pretty' or 'false' if parent is unset)
|
||||||
|
# note: inherits default value from 'format.pretty' or 'false' if parent is unset
|
||||||
|
pretty: false
|
||||||
|
|
||||||
|
# transform any syft-json output to conform to an approximation of the v11.0.1 schema. This includes:
|
||||||
|
# - using the package metadata type names from before v12 of the JSON schema (changed in https://github.com/anchore/syft/pull/1983)
|
||||||
|
#
|
||||||
|
# Note: this will still include package types and fields that were added at or after json schema v12. This means
|
||||||
|
# that output might not strictly be json schema v11 compliant, however, for consumers that require time to port
|
||||||
|
# over to the final syft 1.0 json output this option can be used to ease the transition.
|
||||||
|
#
|
||||||
|
# Note: long term support for this option is not guaranteed.
|
||||||
|
legacy: false
|
||||||
|
|
||||||
|
# all template format options
|
||||||
|
template:
|
||||||
|
# path to the template file to use when rendering the output with the `template` output format.
|
||||||
|
# Note that all template paths are based on the current syft-json schema.
|
||||||
|
# same as -t ; SYFT_TEMPLATE_PATH env var
|
||||||
|
path: ""
|
||||||
|
|
||||||
|
# all spdx-json format options
|
||||||
|
spdx-json:
|
||||||
|
|
||||||
|
# include space indention and newlines
|
||||||
|
# note: inherits default value from 'format.pretty' or 'false' if parent is unset
|
||||||
|
pretty: false
|
||||||
|
|
||||||
|
# all cyclonedx-json format options
|
||||||
|
cyclonedx-json:
|
||||||
|
|
||||||
|
# include space indention and newlines
|
||||||
|
# note: inherits default value from 'format.pretty' or 'false' if parent is unset
|
||||||
|
pretty: false
|
||||||
|
|
||||||
|
# all cyclonedx-xml format options
|
||||||
|
cyclonedx-xml:
|
||||||
|
|
||||||
|
# include space indention
|
||||||
|
# note: inherits default value from 'format.pretty' or 'false' if parent is unset
|
||||||
|
pretty: false
|
||||||
|
|
||||||
|
|
||||||
|
file:
|
||||||
|
|
||||||
|
metadata:
|
||||||
|
# select which files should be captured by the file-metadata cataloger and included in the SBOM.
|
||||||
|
# Options include:
|
||||||
|
# - "all-files": capture all files from the search space
|
||||||
|
# - "owned-files": capture only files owned by packages
|
||||||
|
# - "unowned-files": capture only files not owned by packages
|
||||||
|
# - "no-files", "": do not capture any files
|
||||||
|
# SYFT_FILE_METADATA_SELECTION env var
|
||||||
|
selection: "owned-files"
|
||||||
|
|
||||||
|
# the file digest algorithms to use when cataloging files (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||||
|
# SYFT_FILE_METADATA_DIGESTS env var
|
||||||
|
digests: ["sha256"]
|
||||||
|
|
||||||
|
# capture the contents of select files in the SBOM
|
||||||
|
content:
|
||||||
|
# skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
|
||||||
|
# SYFT_FILE_CONTENT_SKIP_FILES_ABOVE_SIZE env var
|
||||||
|
skip-files-above-size: 1048576
|
||||||
|
|
||||||
|
# file globs for the cataloger to match on
|
||||||
|
# SYFT_FILE_CONTENT_GLOBS env var
|
||||||
|
globs: []
|
||||||
|
|
||||||
|
|
||||||
# cataloging packages is exposed through the packages and power-user subcommands
|
# cataloging packages is exposed through the packages and power-user subcommands
|
||||||
package:
|
package:
|
||||||
|
|
||||||
# search within archives that do contain a file index to search against (zip)
|
# search within archives that do contain a file index to search against (zip)
|
||||||
# note: for now this only applies to the java package cataloger
|
# note: for now this only applies to the java package cataloger
|
||||||
# SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES env var
|
# SYFT_PACKAGE_SEARCH_INDEXED_ARCHIVES env var
|
||||||
@@ -480,102 +672,125 @@ package:
|
|||||||
# SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES env var
|
# SYFT_PACKAGE_SEARCH_UNINDEXED_ARCHIVES env var
|
||||||
search-unindexed-archives: false
|
search-unindexed-archives: false
|
||||||
|
|
||||||
cataloger:
|
# allows users to exclude synthetic binary packages from the sbom
|
||||||
# enable/disable cataloging of packages
|
# these packages are removed if an overlap with a non-synthetic package is found
|
||||||
# SYFT_PACKAGE_CATALOGER_ENABLED env var
|
exclude-binary-overlap-by-ownership: true
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# the search space to look for packages (options: all-layers, squashed)
|
|
||||||
# same as -s ; SYFT_PACKAGE_CATALOGER_SCOPE env var
|
|
||||||
scope: "squashed"
|
|
||||||
|
|
||||||
# cataloging file contents is exposed through the power-user subcommand
|
golang:
|
||||||
file-contents:
|
# search for go package licences in the GOPATH of the system running Syft, note that this is outside the
|
||||||
cataloger:
|
# container filesystem and potentially outside the root of a local directory scan
|
||||||
# enable/disable cataloging of secrets
|
# SYFT_GOLANG_SEARCH_LOCAL_MOD_CACHE_LICENSES env var
|
||||||
# SYFT_FILE_CONTENTS_CATALOGER_ENABLED env var
|
search-local-mod-cache-licenses: false
|
||||||
enabled: true
|
|
||||||
|
# specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod
|
||||||
|
# SYFT_GOLANG_LOCAL_MOD_CACHE_DIR env var
|
||||||
|
local-mod-cache-dir: ""
|
||||||
|
|
||||||
# the search space to look for secrets (options: all-layers, squashed)
|
# search for go package licences by retrieving the package from a network proxy
|
||||||
# SYFT_FILE_CONTENTS_CATALOGER_SCOPE env var
|
# SYFT_GOLANG_SEARCH_REMOTE_LICENSES env var
|
||||||
scope: "squashed"
|
search-remote-licenses: false
|
||||||
|
|
||||||
# skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
|
# remote proxy to use when retrieving go packages from the network,
|
||||||
# SYFT_FILE_CONTENTS_SKIP_FILES_ABOVE_SIZE env var
|
# if unset this defaults to $GOPROXY followed by https://proxy.golang.org
|
||||||
skip-files-above-size: 1048576
|
# SYFT_GOLANG_PROXY env var
|
||||||
|
proxy: ""
|
||||||
|
|
||||||
# file globs for the cataloger to match on
|
# specifies packages which should not be fetched by proxy
|
||||||
# SYFT_FILE_CONTENTS_GLOBS env var
|
# if unset this defaults to $GONOPROXY
|
||||||
globs: []
|
# SYFT_GOLANG_NOPROXY env var
|
||||||
|
no-proxy: ""
|
||||||
|
|
||||||
|
java:
|
||||||
|
maven-url: "https://repo1.maven.org/maven2"
|
||||||
|
max-parent-recursive-depth: 5
|
||||||
|
# enables Syft to use the network to fill in more detailed information about artifacts
|
||||||
|
# currently this enables searching maven-url for license data
|
||||||
|
# when running across pom.xml files that could have more information, syft will
|
||||||
|
# explicitly search maven for license information by querying the online pom when this is true
|
||||||
|
# this option is helpful for when the parent pom has more data,
|
||||||
|
# that is not accessible from within the final built artifact
|
||||||
|
use-network: false
|
||||||
|
|
||||||
# cataloging file metadata is exposed through the power-user subcommand
|
linux-kernel:
|
||||||
file-metadata:
|
# whether to catalog linux kernel modules found within lib/modules/** directories
|
||||||
cataloger:
|
# SYFT_LINUX_KERNEL_CATALOG_MODULES env var
|
||||||
# enable/disable cataloging of file metadata
|
catalog-modules: true
|
||||||
# SYFT_FILE_METADATA_CATALOGER_ENABLED env var
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# the search space to look for file metadata (options: all-layers, squashed)
|
python:
|
||||||
# SYFT_FILE_METADATA_CATALOGER_SCOPE env var
|
# when running across entries in requirements.txt that do not specify a specific version
|
||||||
scope: "squashed"
|
# (e.g. "sqlalchemy >= 1.0.0, <= 2.0.0, != 3.0.0, <= 3.0.0"), attempt to guess what the version could
|
||||||
|
# be based on the version requirements specified (e.g. "1.0.0"). When enabled the lowest expressible version
|
||||||
|
# when given an arbitrary constraint will be used (even if that version may not be available/published).
|
||||||
|
guess-unpinned-requirements: false
|
||||||
|
|
||||||
# the file digest algorithms to use when cataloging files (options: "sha256", "md5", "sha1")
|
javascript:
|
||||||
# SYFT_FILE_METADATA_DIGESTS env var
|
search-remote-licenses: false
|
||||||
digests: ["sha256"]
|
npm-base-url: "https://registry.npmjs.org"
|
||||||
|
|
||||||
# cataloging secrets is exposed through the power-user subcommand
|
|
||||||
secrets:
|
|
||||||
cataloger:
|
|
||||||
# enable/disable cataloging of secrets
|
|
||||||
# SYFT_SECRETS_CATALOGER_ENABLED env var
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# the search space to look for secrets (options: all-layers, squashed)
|
# configuration for the source that the SBOM is generated from (e.g. a file, directory, or container image)
|
||||||
# SYFT_SECRETS_CATALOGER_SCOPE env var
|
source:
|
||||||
scope: "all-layers"
|
# alias name for the source
|
||||||
|
# SYFT_SOURCE_NAME env var; --source-name flag
|
||||||
|
name: ""
|
||||||
|
|
||||||
|
# alias version for the source
|
||||||
|
# SYFT_SOURCE_VERSION env var; --source-version flag
|
||||||
|
version: ""
|
||||||
|
|
||||||
|
# options affecting the file source type
|
||||||
|
file:
|
||||||
|
# the file digest algorithms to use on the scanned file (options: "md5", "sha1", "sha224", "sha256", "sha384", "sha512")
|
||||||
|
digests: ["sha256"]
|
||||||
|
|
||||||
# show extracted secret values in the final JSON report
|
image:
|
||||||
# SYFT_SECRETS_REVEAL_VALUES env var
|
|
||||||
reveal-values: false
|
# allows users to specify which image source should be used to generate the sbom
|
||||||
|
# valid values are: registry, docker, podman
|
||||||
|
# SYFT_SOURCE_IMAGE_DEFAULT_PULL_SOURCE env var
|
||||||
|
default-pull-source: ""
|
||||||
|
|
||||||
# skip searching a file entirely if it is above the given size (default = 1MB; unit = bytes)
|
|
||||||
# SYFT_SECRETS_SKIP_FILES_ABOVE_SIZE env var
|
|
||||||
skip-files-above-size: 1048576
|
|
||||||
|
|
||||||
# name-regex pairs to consider when searching files for secrets. Note: the regex must match single line patterns
|
# options when pulling directly from a registry via the "registry:" or "containerd:" scheme
|
||||||
# but may also have OPTIONAL multiline capture groups. Regexes with a named capture group of "value" will
|
|
||||||
# use the entire regex to match, but the secret value will be assumed to be entirely contained within the
|
|
||||||
# "value" named capture group.
|
|
||||||
additional-patterns: {}
|
|
||||||
|
|
||||||
# names to exclude from the secrets search, valid values are: "aws-access-key", "aws-secret-key", "pem-private-key",
|
|
||||||
# "docker-config-auth", and "generic-api-key". Note: this does not consider any names introduced in the
|
|
||||||
# "secrets.additional-patterns" config option.
|
|
||||||
# SYFT_SECRETS_EXCLUDE_PATTERN_NAMES env var
|
|
||||||
exclude-pattern-names: []
|
|
||||||
|
|
||||||
# options when pulling directly from a registry via the "registry:" scheme
|
|
||||||
registry:
|
registry:
|
||||||
# skip TLS verification when communicating with the registry
|
# skip TLS verification when communicating with the registry
|
||||||
# SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var
|
# SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var
|
||||||
insecure-skip-tls-verify: false
|
insecure-skip-tls-verify: false
|
||||||
|
|
||||||
# use http instead of https when connecting to the registry
|
# use http instead of https when connecting to the registry
|
||||||
# SYFT_REGISTRY_INSECURE_USE_HTTP env var
|
# SYFT_REGISTRY_INSECURE_USE_HTTP env var
|
||||||
insecure-use-http: false
|
insecure-use-http: false
|
||||||
|
|
||||||
|
# filepath to a CA certificate (or directory containing *.crt, *.cert, *.pem) used to generate the client certificate
|
||||||
|
# SYFT_REGISTRY_CA_CERT env var
|
||||||
|
ca-cert: ""
|
||||||
|
|
||||||
# credentials for specific registries
|
# credentials for specific registries
|
||||||
auth:
|
auth:
|
||||||
# the URL to the registry (e.g. "docker.io", "localhost:5000", etc.)
|
# the URL to the registry (e.g. "docker.io", "localhost:5000", etc.)
|
||||||
# SYFT_REGISTRY_AUTH_AUTHORITY env var
|
# SYFT_REGISTRY_AUTH_AUTHORITY env var
|
||||||
- authority: ""
|
- authority: ""
|
||||||
|
|
||||||
# SYFT_REGISTRY_AUTH_USERNAME env var
|
# SYFT_REGISTRY_AUTH_USERNAME env var
|
||||||
username: ""
|
username: ""
|
||||||
|
|
||||||
# SYFT_REGISTRY_AUTH_PASSWORD env var
|
# SYFT_REGISTRY_AUTH_PASSWORD env var
|
||||||
password: ""
|
password: ""
|
||||||
|
|
||||||
# note: token and username/password are mutually exclusive
|
# note: token and username/password are mutually exclusive
|
||||||
# SYFT_REGISTRY_AUTH_TOKEN env var
|
# SYFT_REGISTRY_AUTH_TOKEN env var
|
||||||
token: ""
|
token: ""
|
||||||
# - ... # note, more credentials can be provided via config file only
|
|
||||||
|
# filepath to the client certificate used for TLS authentication to the registry
|
||||||
|
# SYFT_REGISTRY_AUTH_TLS_CERT env var
|
||||||
|
tls-cert: ""
|
||||||
|
|
||||||
|
# filepath to the client key used for TLS authentication to the registry
|
||||||
|
# SYFT_REGISTRY_AUTH_TLS_KEY env var
|
||||||
|
tls-key: ""
|
||||||
|
|
||||||
|
# - ... # note, more credentials can be provided via config file only (not env vars)
|
||||||
|
|
||||||
# generate an attested SBOM
|
# generate an attested SBOM
|
||||||
attest:
|
attest:
|
||||||
|
|||||||
+51
-43
@@ -1,14 +1,49 @@
|
|||||||
# Release Process
|
# Release
|
||||||
|
|
||||||
|
## Creating a release
|
||||||
|
|
||||||
|
This release process itself should be as automated as possible, and has only a few steps:
|
||||||
|
|
||||||
|
1. **Trigger a new release with `make release`**. At this point you'll see a preview
|
||||||
|
changelog in the terminal. If you're happy with the changelog, press `y` to continue, otherwise
|
||||||
|
you can abort and adjust the labels on the PRs and issues to be included in the release and
|
||||||
|
re-run the release trigger command.
|
||||||
|
|
||||||
|
1. A release admin must approve the release on the GitHub Actions release pipeline run page.
|
||||||
|
Once approved, the release pipeline will generate all assets and publish a GitHub Release.
|
||||||
|
|
||||||
|
1. If there is a release Milestone, close it.
|
||||||
|
|
||||||
|
Ideally releasing should be done often with small increments when possible. Unless a
|
||||||
|
breaking change is blocking the release, or no fixes/features have been merged, a good
|
||||||
|
target release cadence is between every 1 or 2 weeks.
|
||||||
|
|
||||||
|
|
||||||
|
## Retracting a release
|
||||||
|
|
||||||
|
If a release is found to be problematic, it can be retracted with the following steps:
|
||||||
|
|
||||||
|
- Deleting the GitHub Release
|
||||||
|
- Untag the docker images in the `ghcr.io` and `docker.io` registries
|
||||||
|
- Revert the brew formula in [`anchore/homebrew-syft`](https://github.com/anchore/homebrew-syft) to point to the previous release
|
||||||
|
- Add a new `retract` entry in the go.mod for the versioned release
|
||||||
|
|
||||||
|
**Note**: do not delete release tags from the git repository since there may already be references to the release
|
||||||
|
in the go proxy, which will cause confusion when trying to reuse the tag later (the H1 hash will not match and there
|
||||||
|
will be a warning when users try to pull the new release).
|
||||||
|
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
A good release process has the following qualities:
|
A good release process has the following qualities:
|
||||||
|
|
||||||
1. There is a way to plan what should be in a release
|
1. There is a way to plan what should be in a release
|
||||||
1. There is a way to see what is actually in a release
|
1. There is a way to see what is actually in a release
|
||||||
1. Allow for different kinds of releases (major breaking vs backwards compatible
|
1. Allow for different kinds of releases (major breaking vs backwards compatible enhancements vs patch updates)
|
||||||
enhancements vs patch updates)
|
|
||||||
1. Specify a repeatable way to build and publish software artifacts
|
1. Specify a repeatable way to build and publish software artifacts
|
||||||
|
|
||||||
## Planning a release
|
|
||||||
|
### Planning a release
|
||||||
|
|
||||||
To indicate a set of features to be released together add each issue to an in-repository
|
To indicate a set of features to be released together add each issue to an in-repository
|
||||||
Milestone named with major-minor version to be released (e.g. `v0.1`). It is OK for other
|
Milestone named with major-minor version to be released (e.g. `v0.1`). It is OK for other
|
||||||
@@ -21,15 +56,13 @@ completed, would allow the release to be considered complete. A Milestone is onl
|
|||||||
|
|
||||||
Not all releases need to be planned. For instance, patch releases for fixes should be
|
Not all releases need to be planned. For instance, patch releases for fixes should be
|
||||||
released when they are ready and when releasing would not interfere with another current
|
released when they are ready and when releasing would not interfere with another current
|
||||||
release (where some partial or breaking features have already been merged). Beta releases
|
release (where some partial or breaking features have already been merged).
|
||||||
and release candidates should not be independently planned from the non-beta release. That
|
|
||||||
is, the features for a `v0.1-beta.1` release should be planned under the `v0.1` Milestone,
|
|
||||||
not under a separate `v0.1-beta.1` Milestone.
|
|
||||||
|
|
||||||
Unless necessary, feature releases should be small and frequent, which may obviate the
|
Unless necessary, feature releases should be small and frequent, which may obviate the
|
||||||
need for regular release planning under a Milestone.
|
need for regular release planning under a Milestone.
|
||||||
|
|
||||||
## What is in a release
|
|
||||||
|
### What is in a release
|
||||||
|
|
||||||
Milestones are specifically for planning a release, not necessarily tracking all changes
|
Milestones are specifically for planning a release, not necessarily tracking all changes
|
||||||
that a release may bring (and more importantly, not all releases are necessarily planned
|
that a release may bring (and more importantly, not all releases are necessarily planned
|
||||||
@@ -42,9 +75,9 @@ Changelog line. Furthermore, there should be a place to see all released version
|
|||||||
release date for each release, the semantic version of the release, and the set of changes
|
release date for each release, the semantic version of the release, and the set of changes
|
||||||
for each release.
|
for each release.
|
||||||
|
|
||||||
This project auto-generates the Changelog contents for each current release and posts the
|
**This project auto-generates the Changelog contents for each current release and posts the
|
||||||
generated contents to the GitHub Release page. Leveraging the GitHub Releases feature
|
generated contents to the GitHub Release page**. Leveraging the GitHub Releases feature
|
||||||
allows GitHub to manage the Changelog on each release outside of the git repository while
|
allows GitHub to manage the Changelog on each release outside of the git source tree while
|
||||||
still being hosted with the released assets.
|
still being hosted with the released assets.
|
||||||
|
|
||||||
The Changelog is generated from the metadata from in-repository issues and PRs, using
|
The Changelog is generated from the metadata from in-repository issues and PRs, using
|
||||||
@@ -60,45 +93,20 @@ The above suggestions imply that we should:
|
|||||||
- The appropriate label is applied to PRs and/or issues to drive specific change type
|
- The appropriate label is applied to PRs and/or issues to drive specific change type
|
||||||
sections (deprecated, breaking, security, bug, etc)
|
sections (deprecated, breaking, security, bug, etc)
|
||||||
|
|
||||||
With this approach as we cultivate good organization of PRs and issues we automatically
|
**With this approach as we cultivate good organization of PRs and issues we automatically
|
||||||
get an equally good Changelog.
|
get an equally good Changelog.**
|
||||||
|
|
||||||
## Major, minor, and patch releases
|
|
||||||
|
### Major, minor, and patch releases
|
||||||
|
|
||||||
The latest version of the tool is the only supported version, which implies that multiple
|
The latest version of the tool is the only supported version, which implies that multiple
|
||||||
parallel release branches will not be a regular process (if ever). Multiple releases can
|
parallel release branches will not be a regular process (if ever). Multiple releases can
|
||||||
be planned in parallel, however, only one can be actively developed at a time. That is, if
|
be planned in parallel, however, only one can be actively developed at a time. That is, if
|
||||||
PRs attached to a release Milestone have been merged into the main branch, that release is
|
PRs attached to a release Milestone have been merged into the main branch, that release is
|
||||||
now the "next" release. This implies that the source of truth for release lies with the
|
now the "next" release. **This implies that the source of truth for release lies with the
|
||||||
git log and Changelog, not with the release Milestones (which are purely for planning and
|
git log and Changelog, not with the release Milestones** (which are purely for planning and
|
||||||
tracking).
|
tracking).
|
||||||
|
|
||||||
Semantic versioning should be used to indicate breaking changes, new features, and fixes.
|
Semantic versioning should be used to indicate breaking changes, new features, and fixes.
|
||||||
The exception to this is `< 1.0`, where the major version is not bumped for breaking changes,
|
The exception to this is `< 1.0`, where the major version is not bumped for breaking changes,
|
||||||
instead the minor version indicates both new features and breaking changes.
|
instead the minor version indicates both new features and breaking changes.
|
||||||
|
|
||||||
## Cutting a release
|
|
||||||
|
|
||||||
Ideally releasing should be done often with small increments when possible. Unless a
|
|
||||||
breaking change is blocking the release, or no fixes/features have been merged, a good
|
|
||||||
target release cadence is between every 2 or 4 weeks.
|
|
||||||
|
|
||||||
This release process itself should be as automated as possible, and have only a few steps:
|
|
||||||
|
|
||||||
1. Tag the main branch with a full semantic-version, prefixed with a `v`. If there is a
|
|
||||||
milestone with a partial version, the full version should be used for the git tag (e.g.
|
|
||||||
with a Milestone of `v0.1` the tag should be `v0.1.0`). You can determine the changes going
|
|
||||||
into a release by running `make changelog-unreleased`. Use this change list to determine the
|
|
||||||
release increment. After determining the release increment (major, minor, patch), create the tag.
|
|
||||||
Given the above example the command to create the tag would be `git tag v0.1.0`.
|
|
||||||
|
|
||||||
1. Push the tag. Given the above example the command to push the tag would be `git push origin v0.1.0`.
|
|
||||||
|
|
||||||
1. A release admin must approve the release on the GitHub Actions release pipeline run page.
|
|
||||||
Once approved, the release pipeline will generate all assets and draft a GitHub Release.
|
|
||||||
|
|
||||||
1. Navigate to the GitHub Release draft page to review the final changelog and publish the
|
|
||||||
release. Once published, a release-follow-up pipeline will publish derivative artifacts
|
|
||||||
(docker image to DockerHub, brew formula to the external homebrew git repo, etc).
|
|
||||||
|
|
||||||
1. If there is a release Milestone, close it.
|
|
||||||
|
|||||||
+479
@@ -0,0 +1,479 @@
|
|||||||
|
|
||||||
|
version: "3"
|
||||||
|
vars:
|
||||||
|
OWNER: anchore
|
||||||
|
PROJECT: syft
|
||||||
|
|
||||||
|
# static file dirs
|
||||||
|
TOOL_DIR: .tool
|
||||||
|
TMP_DIR: .tmp
|
||||||
|
|
||||||
|
# used for changelog generation
|
||||||
|
CHANGELOG: CHANGELOG.md
|
||||||
|
NEXT_VERSION: VERSION
|
||||||
|
|
||||||
|
# used for snapshot builds
|
||||||
|
OS:
|
||||||
|
sh: uname -s | tr '[:upper:]' '[:lower:]'
|
||||||
|
ARCH:
|
||||||
|
sh: |
|
||||||
|
[ "$(uname -m)" = "x86_64" ] && echo "amd64_v1" || echo $(uname -m)
|
||||||
|
PROJECT_ROOT:
|
||||||
|
sh: echo $PWD
|
||||||
|
# note: the snapshot dir must be a relative path starting with ./
|
||||||
|
SNAPSHOT_DIR: ./snapshot
|
||||||
|
SNAPSHOT_BIN: "{{ .PROJECT_ROOT }}/{{ .SNAPSHOT_DIR }}/{{ .OS }}-build_{{ .OS }}_{{ .ARCH }}/{{ .PROJECT }}"
|
||||||
|
SNAPSHOT_CMD: "{{ .TOOL_DIR }}/goreleaser release --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --skip=publish --skip=sign"
|
||||||
|
BUILD_CMD: "{{ .TOOL_DIR }}/goreleaser build --config {{ .TMP_DIR }}/goreleaser.yaml --clean --snapshot --single-target"
|
||||||
|
RELEASE_CMD: "{{ .TOOL_DIR }}/goreleaser release --clean --release-notes {{ .CHANGELOG }}"
|
||||||
|
VERSION:
|
||||||
|
sh: git describe --dirty --always --tags
|
||||||
|
|
||||||
|
# used for install and acceptance testing
|
||||||
|
COMPARE_DIR: ./test/compare
|
||||||
|
COMPARE_TEST_IMAGE: centos:8.2.2004
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
## High-level tasks #################################
|
||||||
|
|
||||||
|
default:
|
||||||
|
desc: Run all validation tasks
|
||||||
|
aliases:
|
||||||
|
- pr-validations
|
||||||
|
- validations
|
||||||
|
cmds:
|
||||||
|
- task: static-analysis
|
||||||
|
- task: test
|
||||||
|
- task: install-test
|
||||||
|
|
||||||
|
static-analysis:
|
||||||
|
desc: Run all static analysis tasks
|
||||||
|
cmds:
|
||||||
|
- task: check-go-mod-tidy
|
||||||
|
- task: check-licenses
|
||||||
|
- task: lint
|
||||||
|
- task: check-json-schema-drift
|
||||||
|
- task: check-binary-fixture-size
|
||||||
|
|
||||||
|
test:
|
||||||
|
desc: Run all levels of test
|
||||||
|
cmds:
|
||||||
|
- task: unit
|
||||||
|
- task: integration
|
||||||
|
- task: validate-cyclonedx-schema
|
||||||
|
- task: benchmark
|
||||||
|
- task: test-utils
|
||||||
|
- task: cli
|
||||||
|
|
||||||
|
## Bootstrap tasks #################################
|
||||||
|
|
||||||
|
binny:
|
||||||
|
internal: true
|
||||||
|
# desc: Get the binny tool
|
||||||
|
generates:
|
||||||
|
- "{{ .TOOL_DIR }}/binny"
|
||||||
|
status:
|
||||||
|
- "test -f {{ .TOOL_DIR }}/binny"
|
||||||
|
cmd: "curl -sSfL https://raw.githubusercontent.com/anchore/binny/main/install.sh | sh -s -- -b .tool"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
tools:
|
||||||
|
desc: Install all tools needed for CI and local development
|
||||||
|
deps: [binny]
|
||||||
|
aliases:
|
||||||
|
- bootstrap
|
||||||
|
generates:
|
||||||
|
- ".binny.yaml"
|
||||||
|
- "{{ .TOOL_DIR }}/*"
|
||||||
|
status:
|
||||||
|
- "{{ .TOOL_DIR }}/binny check -v"
|
||||||
|
cmd: "{{ .TOOL_DIR }}/binny install -v"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
update-tools:
|
||||||
|
desc: Update pinned versions of all tools to their latest available versions
|
||||||
|
deps: [binny]
|
||||||
|
generates:
|
||||||
|
- ".binny.yaml"
|
||||||
|
- "{{ .TOOL_DIR }}/*"
|
||||||
|
cmd: "{{ .TOOL_DIR }}/binny update -v"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
list-tools:
|
||||||
|
desc: List all tools needed for CI and local development
|
||||||
|
deps: [binny]
|
||||||
|
cmd: "{{ .TOOL_DIR }}/binny list"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
list-tool-updates:
|
||||||
|
desc: List all tools that are not up to date relative to the binny config
|
||||||
|
deps: [binny]
|
||||||
|
cmd: "{{ .TOOL_DIR }}/binny list --updates"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
tmpdir:
|
||||||
|
silent: true
|
||||||
|
generates:
|
||||||
|
- "{{ .TMP_DIR }}"
|
||||||
|
cmd: "mkdir -p {{ .TMP_DIR }}"
|
||||||
|
|
||||||
|
## Static analysis tasks #################################
|
||||||
|
|
||||||
|
format:
|
||||||
|
desc: Auto-format all source code
|
||||||
|
deps: [tools]
|
||||||
|
cmds:
|
||||||
|
- gofmt -w -s .
|
||||||
|
- "{{ .TOOL_DIR }}/gosimports -local github.com/anchore -w ."
|
||||||
|
- go mod tidy
|
||||||
|
|
||||||
|
lint-fix:
|
||||||
|
desc: Auto-format all source code + run golangci lint fixers
|
||||||
|
deps: [tools]
|
||||||
|
cmds:
|
||||||
|
- task: format
|
||||||
|
- "{{ .TOOL_DIR }}/golangci-lint run --tests=false --fix"
|
||||||
|
|
||||||
|
lint:
|
||||||
|
desc: Run gofmt + golangci lint checks
|
||||||
|
vars:
|
||||||
|
BAD_FMT_FILES:
|
||||||
|
sh: gofmt -l -s .
|
||||||
|
BAD_FILE_NAMES:
|
||||||
|
sh: "find . | grep -e ':' || true"
|
||||||
|
deps: [tools]
|
||||||
|
cmds:
|
||||||
|
# ensure there are no go fmt differences
|
||||||
|
- cmd: 'test -z "{{ .BAD_FMT_FILES }}" || (echo "files with gofmt issues: [{{ .BAD_FMT_FILES }}]"; exit 1)'
|
||||||
|
silent: true
|
||||||
|
# ensure there are no files with ":" in it (a known back case in the go ecosystem)
|
||||||
|
- cmd: 'test -z "{{ .BAD_FILE_NAMES }}" || (echo "files with bad names: [{{ .BAD_FILE_NAMES }}]"; exit 1)'
|
||||||
|
silent: true
|
||||||
|
# run linting
|
||||||
|
- "{{ .TOOL_DIR }}/golangci-lint run --tests=false"
|
||||||
|
|
||||||
|
|
||||||
|
check-licenses:
|
||||||
|
# desc: Ensure transitive dependencies are compliant with the current license policy
|
||||||
|
deps: [tools]
|
||||||
|
cmd: "{{ .TOOL_DIR }}/bouncer check ./..."
|
||||||
|
|
||||||
|
check-go-mod-tidy:
|
||||||
|
# desc: Ensure go.mod and go.sum are up to date
|
||||||
|
cmds:
|
||||||
|
- cmd: .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
check-json-schema-drift:
|
||||||
|
desc: Ensure there is no drift between the JSON schema and the code
|
||||||
|
cmds:
|
||||||
|
- .github/scripts/json-schema-drift-check.sh
|
||||||
|
|
||||||
|
check-binary-fixture-size:
|
||||||
|
desc: Ensure that the binary test fixtures are not too large
|
||||||
|
cmds:
|
||||||
|
- .github/scripts/check_binary_fixture_size.sh syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets
|
||||||
|
|
||||||
|
|
||||||
|
## Testing tasks #################################
|
||||||
|
|
||||||
|
unit:
|
||||||
|
desc: Run unit tests
|
||||||
|
deps:
|
||||||
|
- tmpdir
|
||||||
|
- fixtures
|
||||||
|
vars:
|
||||||
|
TEST_PKGS:
|
||||||
|
sh: "go list ./... | grep -v {{ .OWNER }}/{{ .PROJECT }}/test | tr '\n' ' '"
|
||||||
|
|
||||||
|
# unit test coverage threshold (in % coverage)
|
||||||
|
COVERAGE_THRESHOLD: 62
|
||||||
|
cmds:
|
||||||
|
- "go test -coverprofile {{ .TMP_DIR }}/unit-coverage-details.txt {{ .TEST_PKGS }}"
|
||||||
|
- cmd: ".github/scripts/coverage.py {{ .COVERAGE_THRESHOLD }} {{ .TMP_DIR }}/unit-coverage-details.txt"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
integration:
|
||||||
|
desc: Run integration tests
|
||||||
|
cmds:
|
||||||
|
- "go test -v ./test/integration"
|
||||||
|
# exercise most of the CLI with the data race detector
|
||||||
|
- "go run -race cmd/syft/main.go alpine:latest"
|
||||||
|
|
||||||
|
validate-cyclonedx-schema:
|
||||||
|
desc: Run integration tests
|
||||||
|
cmds:
|
||||||
|
- "cd schema/cyclonedx && make"
|
||||||
|
|
||||||
|
cli:
|
||||||
|
desc: Run CLI tests
|
||||||
|
# note: we don't want to regenerate the snapshot unless we have to. In CI it's probable
|
||||||
|
# that the cache being restored with the correct binary will be rebuilt since the timestamps
|
||||||
|
# and local checksums will not line up.
|
||||||
|
deps: [tools, snapshot]
|
||||||
|
sources:
|
||||||
|
- "{{ .SNAPSHOT_BIN }}"
|
||||||
|
- ./test/cli/**
|
||||||
|
- ./**/*.go
|
||||||
|
cmds:
|
||||||
|
- cmd: "echo 'testing binary: {{ .SNAPSHOT_BIN }}'"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
- cmd: "test -f {{ .SNAPSHOT_BIN }} || (find {{ .SNAPSHOT_DIR }} && echo '\nno snapshot found' && false)"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
- "go test -count=1 -timeout=15m -v ./test/cli"
|
||||||
|
env:
|
||||||
|
SYFT_BINARY_LOCATION: "{{ .SNAPSHOT_BIN }}"
|
||||||
|
|
||||||
|
test-utils:
|
||||||
|
desc: Run tests for pipeline utils
|
||||||
|
sources:
|
||||||
|
- .github/scripts/labeler*.py
|
||||||
|
cmds:
|
||||||
|
- cmd: python .github/scripts/labeler_test.py
|
||||||
|
|
||||||
|
|
||||||
|
## Benchmark test targets #################################
|
||||||
|
|
||||||
|
benchmark:
|
||||||
|
deps: [tmpdir]
|
||||||
|
sources:
|
||||||
|
- ./**/*.go
|
||||||
|
generates:
|
||||||
|
- "{{ .TMP_DIR }}/benchmark-main.txt"
|
||||||
|
cmds:
|
||||||
|
- "go test -p 1 -run=^Benchmark -bench=. -count=7 -benchmem ./... | tee {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt"
|
||||||
|
- |
|
||||||
|
bash -c "(test -s {{ .TMP_DIR }}/benchmark-main.txt && \
|
||||||
|
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-main.txt {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt || \
|
||||||
|
{{ .TOOL_DIR }}/benchstat {{ .TMP_DIR }}/benchmark-{{ .VERSION }}.txt) \
|
||||||
|
| tee {{ .TMP_DIR }}/benchstat.txt"
|
||||||
|
|
||||||
|
show-benchstat:
|
||||||
|
deps: [benchmark, tmpdir]
|
||||||
|
sources:
|
||||||
|
- "{{ .TMP_DIR }}/benchstat.txt"
|
||||||
|
cmds:
|
||||||
|
- cmd: "cat {{ .TMP_DIR }}/benchstat.txt"
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
|
||||||
|
## Test-fixture-related targets #################################
|
||||||
|
|
||||||
|
fingerprints:
|
||||||
|
desc: Generate test fixture fingerprints
|
||||||
|
generates:
|
||||||
|
- test/integration/test-fixtures/cache.fingerprint
|
||||||
|
- syft/pkg/cataloger/binary/test-fixtures/cache.fingerprint
|
||||||
|
- syft/pkg/cataloger/java/test-fixtures/java-builds/cache.fingerprint
|
||||||
|
- syft/pkg/cataloger/golang/test-fixtures/archs/binaries.fingerprint
|
||||||
|
- syft/pkg/cataloger/redhat/test-fixtures/rpms.fingerprint
|
||||||
|
- syft/pkg/cataloger/kernel/test-fixtures/cache.fingerprint
|
||||||
|
- test/install/cache.fingerprint
|
||||||
|
- test/cli/test-fixtures/cache.fingerprint
|
||||||
|
cmds:
|
||||||
|
# for IMAGE integration test fixtures
|
||||||
|
- "cd test/integration/test-fixtures && make cache.fingerprint"
|
||||||
|
# for BINARY test fixtures
|
||||||
|
- "cd syft/pkg/cataloger/binary/test-fixtures && make cache.fingerprint"
|
||||||
|
# for JAVA BUILD test fixtures
|
||||||
|
- "cd syft/pkg/cataloger/java/test-fixtures/java-builds && make cache.fingerprint"
|
||||||
|
# for GO BINARY test fixtures
|
||||||
|
- "cd syft/pkg/cataloger/golang/test-fixtures/archs && make binaries.fingerprint"
|
||||||
|
# for RPM test fixtures
|
||||||
|
- "cd syft/pkg/cataloger/redhat/test-fixtures && make rpms.fingerprint"
|
||||||
|
# for Kernel test fixtures
|
||||||
|
- "cd syft/pkg/cataloger/kernel/test-fixtures && make cache.fingerprint"
|
||||||
|
# for INSTALL integration test fixtures
|
||||||
|
- "cd test/install && make cache.fingerprint"
|
||||||
|
# for CLI test fixtures
|
||||||
|
- "cd test/cli/test-fixtures && make cache.fingerprint"
|
||||||
|
|
||||||
|
fixtures:
|
||||||
|
desc: Generate test fixtures
|
||||||
|
cmds:
|
||||||
|
- "cd syft/pkg/cataloger/java/test-fixtures/java-builds && make"
|
||||||
|
- "cd syft/pkg/cataloger/redhat/test-fixtures && make"
|
||||||
|
- "cd syft/pkg/cataloger/binary/test-fixtures && make"
|
||||||
|
|
||||||
|
show-test-image-cache:
|
||||||
|
silent: true
|
||||||
|
cmds:
|
||||||
|
- "echo '\nDocker daemon cache:'"
|
||||||
|
- "docker images --format '{{`{{.ID}}`}} {{`{{.Repository}}`}}:{{`{{.Tag}}`}}' | grep stereoscope-fixture- | sort"
|
||||||
|
- "echo '\nTar cache:'"
|
||||||
|
- 'find . -type f -wholename "**/test-fixtures/snapshot/*" | sort'
|
||||||
|
|
||||||
|
|
||||||
|
## install.sh testing targets #################################
|
||||||
|
|
||||||
|
install-test:
|
||||||
|
cmds:
|
||||||
|
- "cd test/install && make"
|
||||||
|
|
||||||
|
install-test-cache-save:
|
||||||
|
cmds:
|
||||||
|
- "cd test/install && make save"
|
||||||
|
|
||||||
|
install-test-cache-load:
|
||||||
|
cmds:
|
||||||
|
- "cd test/install && make load"
|
||||||
|
|
||||||
|
install-test-ci-mac:
|
||||||
|
cmds:
|
||||||
|
- "cd test/install && make ci-test-mac"
|
||||||
|
|
||||||
|
generate-compare-file:
|
||||||
|
cmd: "go run ./cmd/syft {{ .COMPARE_TEST_IMAGE }} -o json > {{ .COMPARE_DIR }}/test-fixtures/acceptance-{{ .COMPARE_TEST_IMAGE }}.json"
|
||||||
|
|
||||||
|
compare-mac:
|
||||||
|
deps: [tmpdir]
|
||||||
|
cmd: |
|
||||||
|
{{ .COMPARE_DIR }}/mac.sh \
|
||||||
|
{{ .SNAPSHOT_DIR }} \
|
||||||
|
{{ .COMPARE_DIR }} \
|
||||||
|
{{ .COMPARE_TEST_IMAGE }} \
|
||||||
|
{{ .TMP_DIR }}
|
||||||
|
|
||||||
|
compare-linux:
|
||||||
|
cmds:
|
||||||
|
- task: compare-test-deb-package-install
|
||||||
|
- task: compare-test-rpm-package-install
|
||||||
|
|
||||||
|
compare-test-deb-package-install:
|
||||||
|
deps: [tmpdir]
|
||||||
|
cmd: |
|
||||||
|
{{ .COMPARE_DIR }}/deb.sh \
|
||||||
|
{{ .SNAPSHOT_DIR }} \
|
||||||
|
{{ .COMPARE_DIR }} \
|
||||||
|
{{ .COMPARE_TEST_IMAGE }} \
|
||||||
|
{{ .TMP_DIR }}
|
||||||
|
|
||||||
|
compare-test-rpm-package-install:
|
||||||
|
deps: [tmpdir]
|
||||||
|
cmd: |
|
||||||
|
{{ .COMPARE_DIR }}/rpm.sh \
|
||||||
|
{{ .SNAPSHOT_DIR }} \
|
||||||
|
{{ .COMPARE_DIR }} \
|
||||||
|
{{ .COMPARE_TEST_IMAGE }} \
|
||||||
|
{{ .TMP_DIR }}
|
||||||
|
|
||||||
|
|
||||||
|
## Code and data generation targets #################################
|
||||||
|
|
||||||
|
generate:
|
||||||
|
desc: Add data generation tasks
|
||||||
|
cmds:
|
||||||
|
- task: generate-json-schema
|
||||||
|
- task: generate-license-list
|
||||||
|
- task: generate-cpe-dictionary-index
|
||||||
|
|
||||||
|
generate-json-schema:
|
||||||
|
desc: Generate a new JSON schema
|
||||||
|
cmds:
|
||||||
|
- "cd syft/internal && go generate . && cd jsonschema && go run . && go fmt ../..."
|
||||||
|
|
||||||
|
generate-license-list:
|
||||||
|
desc: Generate an updated license processing code off of the latest available SPDX license list
|
||||||
|
cmds:
|
||||||
|
- "go generate ./internal/spdxlicense/..."
|
||||||
|
- "gofmt -s -w ./internal/spdxlicense"
|
||||||
|
|
||||||
|
generate-cpe-dictionary-index:
|
||||||
|
desc: Generate the CPE index based off of the latest available CPE dictionary
|
||||||
|
dir: "syft/pkg/cataloger/common/cpe/dictionary"
|
||||||
|
cmds:
|
||||||
|
- "go generate"
|
||||||
|
|
||||||
|
|
||||||
|
## Build-related targets #################################
|
||||||
|
|
||||||
|
build:
|
||||||
|
desc: Build the project
|
||||||
|
deps: [tools, tmpdir]
|
||||||
|
generates:
|
||||||
|
- "{{ .PROJECT }}"
|
||||||
|
cmds:
|
||||||
|
- silent: true
|
||||||
|
cmd: |
|
||||||
|
echo "dist: {{ .SNAPSHOT_DIR }}" > {{ .TMP_DIR }}/goreleaser.yaml
|
||||||
|
cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml
|
||||||
|
|
||||||
|
- "{{ .BUILD_CMD }}"
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
desc: Create a snapshot release
|
||||||
|
aliases:
|
||||||
|
- build
|
||||||
|
deps: [tools, tmpdir]
|
||||||
|
sources:
|
||||||
|
- cmd/**/*.go
|
||||||
|
- syft/**/*.go
|
||||||
|
- internal/**/*.go
|
||||||
|
method: checksum
|
||||||
|
generates:
|
||||||
|
- "{{ .SNAPSHOT_BIN }}"
|
||||||
|
cmds:
|
||||||
|
- silent: true
|
||||||
|
cmd: |
|
||||||
|
echo "dist: {{ .SNAPSHOT_DIR }}" > {{ .TMP_DIR }}/goreleaser.yaml
|
||||||
|
cat .goreleaser.yaml >> {{ .TMP_DIR }}/goreleaser.yaml
|
||||||
|
|
||||||
|
- "{{ .SNAPSHOT_CMD }}"
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
desc: Generate a changelog
|
||||||
|
deps: [tools]
|
||||||
|
generates:
|
||||||
|
- "{{ .CHANGELOG }}"
|
||||||
|
- "{{ .NEXT_VERSION }}"
|
||||||
|
cmds:
|
||||||
|
- "{{ .TOOL_DIR }}/chronicle -vv -n --version-file {{ .NEXT_VERSION }} > {{ .CHANGELOG }}"
|
||||||
|
- "{{ .TOOL_DIR }}/glow {{ .CHANGELOG }}"
|
||||||
|
|
||||||
|
|
||||||
|
## Release targets #################################
|
||||||
|
|
||||||
|
release:
|
||||||
|
desc: Create a release
|
||||||
|
interactive: true
|
||||||
|
deps: [tools]
|
||||||
|
cmds:
|
||||||
|
- cmd: .github/scripts/trigger-release.sh
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
|
||||||
|
## CI-only targets #################################
|
||||||
|
|
||||||
|
ci-check:
|
||||||
|
# desc: "[CI only] Are you in CI?"
|
||||||
|
cmds:
|
||||||
|
- cmd: .github/scripts/ci-check.sh
|
||||||
|
silent: true
|
||||||
|
|
||||||
|
ci-release:
|
||||||
|
# desc: "[CI only] Create a release"
|
||||||
|
deps: [tools]
|
||||||
|
cmds:
|
||||||
|
- task: ci-check
|
||||||
|
- "{{ .TOOL_DIR }}/chronicle -vvv > CHANGELOG.md"
|
||||||
|
- cmd: "cat CHANGELOG.md"
|
||||||
|
silent: true
|
||||||
|
- "{{ .RELEASE_CMD }}"
|
||||||
|
|
||||||
|
|
||||||
|
## Cleanup targets #################################
|
||||||
|
|
||||||
|
clean-snapshot:
|
||||||
|
desc: Remove any snapshot builds
|
||||||
|
cmds:
|
||||||
|
- "rm -rf {{ .SNAPSHOT_DIR }}"
|
||||||
|
- "rm -rf {{ .TMP_DIR }}/goreleaser.yaml"
|
||||||
|
|
||||||
|
clean-cache:
|
||||||
|
desc: Remove all docker cache and local image tar cache
|
||||||
|
cmds:
|
||||||
|
- 'find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" -delete'
|
||||||
|
- "docker images --format '{{`{{.ID}}`}} {{`{{.Repository}}`}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs -r docker rmi --force"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# See documentation here: https://github.com/artifacthub/hub/blob/v1.6.0/docs/metadata/artifacthub-repo.yml
|
# See documentation here: https://github.com/artifacthub/hub/blob/v1.6.0/docs/metadata/artifacthub-repo.yml
|
||||||
|
repositoryID: eced152f-b15d-4879-8b3b-1175397192ba
|
||||||
owners:
|
owners:
|
||||||
- name: wagoodman
|
- name: wagoodman
|
||||||
email: wagoodman@gmail.com
|
email: wagoodman@gmail.com
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/cmd/syft/cli/attest"
|
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
"github.com/anchore/syft/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] --key [KEY] alpine:latest
|
|
||||||
Supports the following image sources:
|
|
||||||
{{.appName}} {{.command}} --key [KEY] yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
|
||||||
{{.appName}} {{.command}} --key [KEY] path/to/a/file/or/dir only for OCI tar or OCI directory
|
|
||||||
`
|
|
||||||
attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp
|
|
||||||
|
|
||||||
attestHelp = attestExample + attestSchemeHelp
|
|
||||||
)
|
|
||||||
|
|
||||||
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
|
|
||||||
ao := options.AttestOptions{}
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "attest --output [FORMAT] --key [KEY] [SOURCE]",
|
|
||||||
Short: "Generate a package SBOM as an attestation for the given [SOURCE] container image",
|
|
||||||
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation",
|
|
||||||
Example: internal.Tprintf(attestHelp, map[string]interface{}{
|
|
||||||
"appName": internal.ApplicationName,
|
|
||||||
"command": "attest",
|
|
||||||
}),
|
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
|
||||||
// run to unmarshal viper object onto app config
|
|
||||||
// the viper object correctly
|
|
||||||
if err := app.LoadAllValues(v, ro.Config); err != nil {
|
|
||||||
return fmt.Errorf("invalid application config: %w", err)
|
|
||||||
}
|
|
||||||
// configure logging for command
|
|
||||||
newLogWrapper(app)
|
|
||||||
logApplicationConfig(app)
|
|
||||||
return validateArgs(cmd, args)
|
|
||||||
},
|
|
||||||
SilenceUsage: true,
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
// this MUST be called first to make sure app config decodes
|
|
||||||
// the viper object correctly
|
|
||||||
if app.CheckForAppUpdate {
|
|
||||||
checkForApplicationUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// build cosign key options for attestation
|
|
||||||
ko := sigopts.KeyOpts{
|
|
||||||
KeyRef: app.Attest.KeyRef,
|
|
||||||
FulcioURL: app.Attest.FulcioURL,
|
|
||||||
IDToken: app.Attest.FulcioIdentityToken,
|
|
||||||
InsecureSkipFulcioVerify: app.Attest.InsecureSkipFulcioVerify,
|
|
||||||
RekorURL: app.Attest.RekorURL,
|
|
||||||
OIDCIssuer: app.Attest.OIDCIssuer,
|
|
||||||
OIDCClientID: app.Attest.OIDCClientID,
|
|
||||||
OIDCRedirectURL: app.Attest.OIDCRedirectURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
return attest.Run(cmd.Context(), app, ko, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := ao.AddFlags(cmd, v)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -1,398 +0,0 @@
|
|||||||
package attest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
|
||||||
"github.com/in-toto/in-toto-golang/in_toto"
|
|
||||||
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
|
|
||||||
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
|
|
||||||
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
|
|
||||||
"github.com/sigstore/cosign/pkg/cosign"
|
|
||||||
"github.com/sigstore/cosign/pkg/cosign/attestation"
|
|
||||||
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
|
|
||||||
"github.com/sigstore/cosign/pkg/oci/mutate"
|
|
||||||
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
|
|
||||||
"github.com/sigstore/cosign/pkg/oci/static"
|
|
||||||
sigs "github.com/sigstore/cosign/pkg/signature"
|
|
||||||
"github.com/sigstore/cosign/pkg/types"
|
|
||||||
"github.com/sigstore/rekor/pkg/generated/client"
|
|
||||||
"github.com/sigstore/rekor/pkg/generated/models"
|
|
||||||
"github.com/sigstore/sigstore/pkg/signature/dsse"
|
|
||||||
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
"github.com/wagoodman/go-progress"
|
|
||||||
|
|
||||||
"github.com/anchore/stereoscope"
|
|
||||||
"github.com/anchore/stereoscope/pkg/image"
|
|
||||||
"github.com/anchore/syft/cmd/syft/cli/eventloop"
|
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
|
||||||
"github.com/anchore/syft/cmd/syft/cli/packages"
|
|
||||||
"github.com/anchore/syft/internal/bus"
|
|
||||||
"github.com/anchore/syft/internal/config"
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
"github.com/anchore/syft/internal/ui"
|
|
||||||
"github.com/anchore/syft/syft"
|
|
||||||
"github.com/anchore/syft/syft/event"
|
|
||||||
"github.com/anchore/syft/syft/formats/cyclonedxjson"
|
|
||||||
"github.com/anchore/syft/syft/formats/spdxjson"
|
|
||||||
"github.com/anchore/syft/syft/formats/syftjson"
|
|
||||||
"github.com/anchore/syft/syft/sbom"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
allowedAttestFormats = []sbom.FormatID{
|
|
||||||
syftjson.ID,
|
|
||||||
spdxjson.ID,
|
|
||||||
cyclonedxjson.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
intotoJSONDsseType = `application/vnd.in-toto+json`
|
|
||||||
)
|
|
||||||
|
|
||||||
func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args []string) error {
|
|
||||||
// We cannot generate an attestation for more than one output
|
|
||||||
if len(app.Outputs) > 1 {
|
|
||||||
return fmt.Errorf("unable to generate attestation for more than one output")
|
|
||||||
}
|
|
||||||
|
|
||||||
// can only be an image for attestation or OCI DIR
|
|
||||||
userInput := args[0]
|
|
||||||
si, err := parseImageSource(userInput, app)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
output := parseAttestationOutput(app.Outputs)
|
|
||||||
|
|
||||||
format := syft.FormatByName(output)
|
|
||||||
|
|
||||||
// user typo or unknown outputs provided
|
|
||||||
if format == nil {
|
|
||||||
format = syft.FormatByID(syftjson.ID) // default attestation format
|
|
||||||
}
|
|
||||||
predicateType := formatPredicateType(format)
|
|
||||||
if predicateType == "" {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"could not produce attestation predicate for given format: %q. Available formats: %+v",
|
|
||||||
options.FormatAliases(format.ID()),
|
|
||||||
options.FormatAliases(allowedAttestFormats...),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.Attest.KeyRef != "" {
|
|
||||||
passFunc, err := selectPassFunc(app.Attest.KeyRef, app.Attest.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ko.PassFunc = passFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
sv, err := sign.SignerFromKeyOpts(ctx, "", "", ko)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer sv.Close()
|
|
||||||
|
|
||||||
eventBus := partybus.NewBus()
|
|
||||||
stereoscope.SetBus(eventBus)
|
|
||||||
syft.SetBus(eventBus)
|
|
||||||
subscription := eventBus.Subscribe()
|
|
||||||
|
|
||||||
return eventloop.EventLoop(
|
|
||||||
execWorker(app, *si, format, predicateType, sv),
|
|
||||||
eventloop.SetupSignals(),
|
|
||||||
subscription,
|
|
||||||
stereoscope.Cleanup,
|
|
||||||
ui.Select(options.IsVerbose(app), app.Quiet)...,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseAttestationOutput(outputs []string) (format string) {
|
|
||||||
if len(outputs) == 0 {
|
|
||||||
outputs = append(outputs, string(syftjson.ID))
|
|
||||||
}
|
|
||||||
|
|
||||||
return outputs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseImageSource(userInput string, app *config.Application) (s *source.Input, err error) {
|
|
||||||
si, err := source.ParseInputWithName(userInput, app.Platform, false, app.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not generate source input for attest command: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch si.Scheme {
|
|
||||||
case source.ImageScheme, source.UnknownScheme:
|
|
||||||
// at this point we know that it cannot be dir: or file: schemes;
|
|
||||||
// we will assume that the unknown scheme could represent an image;
|
|
||||||
si.Scheme = source.ImageScheme
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("attest command can only be used with image sources but discovered %q when given %q", si.Scheme, userInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the original detection was from the local daemon we want to short circuit
|
|
||||||
// that and attempt to generate the image source from its current registry source instead
|
|
||||||
switch si.ImageSource {
|
|
||||||
case image.UnknownSource, image.OciRegistrySource:
|
|
||||||
si.ImageSource = image.OciRegistrySource
|
|
||||||
case image.SingularitySource:
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("attest command can only be used with image sources fetch directly from the registry, but discovered an image source of %q when given %q", si.ImageSource, userInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
return si, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func execWorker(app *config.Application, sourceInput source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error {
|
|
||||||
errs := make(chan error)
|
|
||||||
go func() {
|
|
||||||
defer close(errs)
|
|
||||||
|
|
||||||
src, cleanup, err := source.NewFromRegistry(sourceInput, app.Registry.ToOptions(), app.Exclusions)
|
|
||||||
if cleanup != nil {
|
|
||||||
defer cleanup()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
errs <- fmt.Errorf("failed to construct source from user input %q: %w", sourceInput.UserInput, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := packages.GenerateSBOM(src, errs, app)
|
|
||||||
if err != nil {
|
|
||||||
errs <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sbomBytes, err := syft.Encode(*s, format)
|
|
||||||
if err != nil {
|
|
||||||
errs <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
signedPayload, err := generateAttestation(sourceInput, sbomBytes, src, sv, predicateType)
|
|
||||||
if err != nil {
|
|
||||||
errs <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = publishAttestation(app, signedPayload, src, sv)
|
|
||||||
if err != nil {
|
|
||||||
errs <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
|
||||||
Type: event.Exit,
|
|
||||||
Value: func() error {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateAttestation(si source.Input, predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) ([]byte, error) {
|
|
||||||
var h v1.Hash
|
|
||||||
|
|
||||||
switch si.ImageSource {
|
|
||||||
case image.OciRegistrySource:
|
|
||||||
switch len(src.Image.Metadata.RepoDigests) {
|
|
||||||
case 0:
|
|
||||||
return nil, fmt.Errorf("cannot generate attestation since no repo digests were found; make sure you're passing an OCI registry source for the attest command")
|
|
||||||
case 1:
|
|
||||||
d, err := name.NewDigest(src.Image.Metadata.RepoDigests[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err = v1.NewHash(d.Identifier())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("cannot generate attestation since multiple repo digests were found for the image: %+v", src.Image.Metadata.RepoDigests)
|
|
||||||
}
|
|
||||||
|
|
||||||
case image.SingularitySource:
|
|
||||||
var err error
|
|
||||||
h, err = v1.NewHash(src.Image.Metadata.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
|
|
||||||
Predicate: bytes.NewBuffer(predicate),
|
|
||||||
Type: predicateType,
|
|
||||||
Digest: h.Hex,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := json.Marshal(sh)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
|
|
||||||
return wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(context.Background()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// publishAttestation publishes signedPayload to the location specified by the user.
|
|
||||||
func publishAttestation(app *config.Application, signedPayload []byte, src *source.Source, sv *sign.SignerVerifier) error {
|
|
||||||
switch {
|
|
||||||
// We want to give the option to not upload the generated attestation
|
|
||||||
// if passed or if the user is using local PKI
|
|
||||||
case app.Attest.NoUpload || app.Attest.KeyRef != "":
|
|
||||||
if app.File != "" {
|
|
||||||
return os.WriteFile(app.File, signedPayload, 0600)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := os.Stdout.Write(signedPayload)
|
|
||||||
return err
|
|
||||||
|
|
||||||
default:
|
|
||||||
ref, err := name.ParseReference(src.Metadata.ImageMetadata.UserInput)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
digest, err := ociremote.ResolveDigest(ref)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return uploadAttestation(app, signedPayload, digest, sv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func trackUploadAttestation() (*progress.Stage, *progress.Manual) {
|
|
||||||
stage := &progress.Stage{}
|
|
||||||
prog := &progress.Manual{}
|
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
|
||||||
Type: event.UploadAttestation,
|
|
||||||
Value: progress.StagedProgressable(&struct {
|
|
||||||
progress.Stager
|
|
||||||
progress.Progressable
|
|
||||||
}{
|
|
||||||
Stager: stage,
|
|
||||||
Progressable: prog,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
return stage, prog
|
|
||||||
}
|
|
||||||
|
|
||||||
// uploads signed SBOM payload to Rekor transparency log along with key information;
|
|
||||||
// returns a bundle for attestation annotations
|
|
||||||
// rekor bundle includes a signed payload and rekor timestamp;
|
|
||||||
// the bundle is then wrapped onto an OCI signed entity and uploaded to
|
|
||||||
// the user's image's OCI registry repository as *.att
|
|
||||||
func uploadAttestation(app *config.Application, signedPayload []byte, digest name.Digest, sv *sign.SignerVerifier) error {
|
|
||||||
// add application/vnd.dsse.envelope.v1+json as media type for other applications to decode attestation
|
|
||||||
opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)}
|
|
||||||
if sv.Cert != nil {
|
|
||||||
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
|
|
||||||
}
|
|
||||||
|
|
||||||
stage, prog := trackUploadAttestation()
|
|
||||||
defer prog.SetCompleted() // just in case we return early
|
|
||||||
|
|
||||||
prog.Total = 2
|
|
||||||
stage.Current = "uploading signing information to transparency log"
|
|
||||||
|
|
||||||
// uploads payload to Rekor transparency log along with key information;
|
|
||||||
// returns bundle for attesation annotations
|
|
||||||
// rekor bundle includes a signed payload and rekor timestamp;
|
|
||||||
// the bundle is then wrapped onto an OCI signed entity and uploaded to
|
|
||||||
// the user's image's OCI registry repository as *.att
|
|
||||||
bundle, err := uploadToTlog(context.TODO(), sv, app.Attest.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
|
|
||||||
return cosign.TLogUploadInTotoAttestation(context.TODO(), r, signedPayload, b)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
prog.N = 1
|
|
||||||
stage.Current = "uploading attestation to OCI registry"
|
|
||||||
|
|
||||||
// add bundle OCI attestation that is uploaded to
|
|
||||||
opts = append(opts, static.WithBundle(bundle))
|
|
||||||
sig, err := static.NewAttestation(signedPayload, opts...)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
se, err := ociremote.SignedEntity(digest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
newSE, err := mutate.AttachAttestationToEntity(se, sig)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publish the attestations associated with this entity
|
|
||||||
err = ociremote.WriteAttestations(digest.Repository, newSE)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
prog.SetCompleted()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatPredicateType(format sbom.Format) string {
|
|
||||||
switch format.ID() {
|
|
||||||
case spdxjson.ID:
|
|
||||||
return in_toto.PredicateSPDX
|
|
||||||
case cyclonedxjson.ID:
|
|
||||||
return in_toto.PredicateCycloneDX
|
|
||||||
case syftjson.ID:
|
|
||||||
return "https://syft.dev/bom"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)
|
|
||||||
|
|
||||||
func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) {
|
|
||||||
var rekorBytes []byte
|
|
||||||
// Upload the cert or the public key, depending on what we have
|
|
||||||
if sv.Cert != nil {
|
|
||||||
rekorBytes = sv.Cert
|
|
||||||
} else {
|
|
||||||
pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rekorBytes = pemBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
rekorClient, err := rekor.NewClient(rekorURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
entry, err := upload(rekorClient, rekorBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry.LogIndex != nil {
|
|
||||||
log.Debugf("transparency log entry created with index: %v", *entry.LogIndex)
|
|
||||||
}
|
|
||||||
return cbundle.EntryToBundle(entry), nil
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package attest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sigstore/cosign/pkg/cosign"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func selectPassFunc(keypath, password string) (cosign.PassFunc, error) {
|
|
||||||
keyContents, err := os.ReadFile(keypath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var fn cosign.PassFunc = func(bool) (b []byte, err error) { return nil, nil }
|
|
||||||
|
|
||||||
_, err = cosign.LoadPrivateKey(keyContents, nil)
|
|
||||||
if err != nil {
|
|
||||||
fn = func(bool) (b []byte, err error) {
|
|
||||||
return fetchPassword(password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchPassword(password string) (b []byte, err error) {
|
|
||||||
potentiallyPipedInput, err := internal.IsPipedInput()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("unable to determine if there is piped input: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case password != "":
|
|
||||||
return []byte(password), nil
|
|
||||||
case potentiallyPipedInput:
|
|
||||||
// handle piped in passwords
|
|
||||||
pwBytes, err := io.ReadAll(os.Stdin)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to get password from stdin: %w", err)
|
|
||||||
}
|
|
||||||
// be resilient to input that may have newline characters (in case someone is using echo without -n)
|
|
||||||
cleanPw := strings.TrimRight(string(pwBytes), "\n")
|
|
||||||
return []byte(cleanPw), nil
|
|
||||||
case internal.IsTerminal():
|
|
||||||
return cosign.GetPassFromTerm(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.New("no method available to fetch password")
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/stereoscope"
|
||||||
|
"github.com/anchore/syft/cmd/syft/cli/commands"
|
||||||
|
handler "github.com/anchore/syft/cmd/syft/cli/ui"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
|
"github.com/anchore/syft/internal/bus"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/internal/redact"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Application constructs the `syft packages` command and aliases the root command to `syft packages`.
|
||||||
|
// It is also responsible for organizing flag usage and injecting the application config for each command.
|
||||||
|
// It also constructs the syft attest command and the syft version command.
|
||||||
|
// `RunE` is the earliest that the complete application configuration can be loaded.
|
||||||
|
func Application(id clio.Identification) clio.Application {
|
||||||
|
app, _ := create(id, os.Stdout)
|
||||||
|
return app
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command returns the root command for the syft CLI application. This is useful for embedding the entire syft CLI
|
||||||
|
// into an existing application.
|
||||||
|
func Command(id clio.Identification) *cobra.Command {
|
||||||
|
_, cmd := create(id, os.Stdout)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(id clio.Identification, out io.Writer) (clio.Application, *cobra.Command) {
|
||||||
|
clioCfg := clio.NewSetupConfig(id).
|
||||||
|
WithGlobalConfigFlag(). // add persistent -c <path> for reading an application config from
|
||||||
|
WithGlobalLoggingFlags(). // add persistent -v and -q flags tied to the logging config
|
||||||
|
WithConfigInRootHelp(). // --help on the root command renders the full application config in the help text
|
||||||
|
WithUIConstructor(
|
||||||
|
// select a UI based on the logging configuration and state of stdin (if stdin is a tty)
|
||||||
|
func(cfg clio.Config) ([]clio.UI, error) {
|
||||||
|
noUI := ui.None(out, cfg.Log.Quiet)
|
||||||
|
if !cfg.Log.AllowUI(os.Stdin) || cfg.Log.Quiet {
|
||||||
|
return []clio.UI{noUI}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []clio.UI{
|
||||||
|
ui.New(out, cfg.Log.Quiet,
|
||||||
|
handler.New(handler.DefaultHandlerConfig()),
|
||||||
|
),
|
||||||
|
noUI,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
).
|
||||||
|
WithInitializers(
|
||||||
|
func(state *clio.State) error {
|
||||||
|
// clio is setting up and providing the bus, redact store, and logger to the application. Once loaded,
|
||||||
|
// we can hoist them into the internal packages for global use.
|
||||||
|
stereoscope.SetBus(state.Bus)
|
||||||
|
bus.Set(state.Bus)
|
||||||
|
|
||||||
|
redact.Set(state.RedactStore)
|
||||||
|
|
||||||
|
log.Set(state.Logger)
|
||||||
|
stereoscope.SetLogger(state.Logger)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
).
|
||||||
|
WithPostRuns(func(state *clio.State, err error) {
|
||||||
|
stereoscope.Cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
app := clio.New(*clioCfg)
|
||||||
|
|
||||||
|
// since root is aliased as the packages cmd we need to construct this command first
|
||||||
|
// we also need the command to have information about the `root` options because of this alias
|
||||||
|
scanCmd := commands.Scan(app)
|
||||||
|
|
||||||
|
// root is currently an alias for the scan command
|
||||||
|
rootCmd := commands.Root(app, scanCmd)
|
||||||
|
|
||||||
|
// add sub-commands
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
scanCmd,
|
||||||
|
commands.Packages(app, scanCmd), // this is currently an alias for the scan command
|
||||||
|
commands.Attest(app),
|
||||||
|
commands.Convert(app),
|
||||||
|
clio.VersionCommand(id),
|
||||||
|
cranecmd.NewCmdAuthLogin(id.Name), // syft login uses the same command as crane
|
||||||
|
)
|
||||||
|
|
||||||
|
// note: we would direct cobra to use our writer explicitly with rootCmd.SetOut(out) , however this causes
|
||||||
|
// deprecation warnings to be shown to stdout via the writer instead of stderr. This is unfortunate since this
|
||||||
|
// does not appear to be the correct behavior on cobra's part https://github.com/spf13/cobra/issues/1708 .
|
||||||
|
// In the future this functionality should be restored.
|
||||||
|
|
||||||
|
return app, rootCmd
|
||||||
|
}
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
cranecmd "github.com/google/go-containerregistry/cmd/crane/cmd"
|
|
||||||
"github.com/gookit/color"
|
|
||||||
logrusUpstream "github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
|
|
||||||
"github.com/anchore/go-logger/adapter/logrus"
|
|
||||||
"github.com/anchore/stereoscope"
|
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
"github.com/anchore/syft/internal/bus"
|
|
||||||
"github.com/anchore/syft/internal/config"
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
"github.com/anchore/syft/internal/version"
|
|
||||||
"github.com/anchore/syft/syft"
|
|
||||||
"github.com/anchore/syft/syft/event"
|
|
||||||
)
|
|
||||||
|
|
||||||
const indent = " "
|
|
||||||
|
|
||||||
// New constructs the `syft packages` command, aliases the root command to `syft packages`,
|
|
||||||
// and constructs the `syft power-user` and `syft attest` commands. It is also responsible for
|
|
||||||
// organizing flag usage and injecting the application config for each command.
|
|
||||||
// Because of how the `cobra` library behaves, the application's configuration is initialized
|
|
||||||
// at this level. Values from the config should only be used after `app.LoadAllValues` has been called.
|
|
||||||
// Cobra does not have knowledge of the user provided flags until the `RunE` block of each command.
|
|
||||||
// `RunE` is the earliest that the complete application configuration can be loaded.
|
|
||||||
func New() (*cobra.Command, error) {
|
|
||||||
app := &config.Application{}
|
|
||||||
|
|
||||||
// allow for nested options to be specified via environment variables
|
|
||||||
// e.g. pod.context = APPNAME_POD_CONTEXT
|
|
||||||
v := viper.NewWithOptions(viper.EnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")))
|
|
||||||
|
|
||||||
// since root is aliased as the packages cmd we need to construct this command first
|
|
||||||
// we also need the command to have information about the `root` options because of this alias
|
|
||||||
ro := &options.RootOptions{}
|
|
||||||
po := &options.PackagesOptions{}
|
|
||||||
packagesCmd := Packages(v, app, ro, po)
|
|
||||||
|
|
||||||
// root options are also passed to the attestCmd so that a user provided config location can be discovered
|
|
||||||
attestCmd := Attest(v, app, ro)
|
|
||||||
poweruserCmd := PowerUser(v, app, ro)
|
|
||||||
convertCmd := Convert(v, app, ro, po)
|
|
||||||
|
|
||||||
// rootCmd is currently an alias for the packages command
|
|
||||||
rootCmd := &cobra.Command{
|
|
||||||
Short: packagesCmd.Short,
|
|
||||||
Long: packagesCmd.Long,
|
|
||||||
Args: packagesCmd.Args,
|
|
||||||
Example: packagesCmd.Example,
|
|
||||||
SilenceUsage: true,
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: packagesCmd.RunE,
|
|
||||||
Version: version.FromBuild().Version,
|
|
||||||
}
|
|
||||||
rootCmd.SetVersionTemplate(fmt.Sprintf("%s {{.Version}}\n", internal.ApplicationName))
|
|
||||||
|
|
||||||
// start adding flags to all the commands
|
|
||||||
err := ro.AddFlags(rootCmd, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// package flags need to be decorated onto the rootCmd so that rootCmd can function as a packages alias
|
|
||||||
err = po.AddFlags(rootCmd, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// attest also uses flags from the packagesCmd since it generates an sbom
|
|
||||||
err = po.AddFlags(attestCmd, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// poweruser also uses the packagesCmd flags since it is a specialized version of the command
|
|
||||||
err = po.AddFlags(poweruserCmd, v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// commands to add to root
|
|
||||||
cmds := []*cobra.Command{
|
|
||||||
packagesCmd,
|
|
||||||
attestCmd,
|
|
||||||
convertCmd,
|
|
||||||
poweruserCmd,
|
|
||||||
poweruserCmd,
|
|
||||||
Completion(),
|
|
||||||
Version(v, app),
|
|
||||||
cranecmd.NewCmdAuthLogin("syft"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add sub-commands.
|
|
||||||
for _, cmd := range cmds {
|
|
||||||
rootCmd.AddCommand(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rootCmd, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateArgs(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
|
|
||||||
if err := cmd.Help(); err != nil {
|
|
||||||
return fmt.Errorf("unable to display help: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("an image/directory argument is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return cobra.MaximumNArgs(1)(cmd, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkForApplicationUpdate() {
|
|
||||||
log.Debugf("checking if a new version of %s is available", internal.ApplicationName)
|
|
||||||
isAvailable, newVersion, err := version.IsUpdateAvailable()
|
|
||||||
if err != nil {
|
|
||||||
// this should never stop the application
|
|
||||||
log.Errorf(err.Error())
|
|
||||||
}
|
|
||||||
if isAvailable {
|
|
||||||
log.Infof("new version of %s is available: %s (current version is %s)", internal.ApplicationName, newVersion, version.FromBuild().Version)
|
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
|
||||||
Type: event.AppUpdateAvailable,
|
|
||||||
Value: newVersion,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log.Debugf("no new %s update available", internal.ApplicationName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func logApplicationConfig(app *config.Application) {
|
|
||||||
versionInfo := version.FromBuild()
|
|
||||||
log.Infof("%s version: %+v", internal.ApplicationName, versionInfo.Version)
|
|
||||||
log.Debugf("application config:\n%+v", color.Magenta.Sprint(app.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLogWrapper(app *config.Application) {
|
|
||||||
cfg := logrus.Config{
|
|
||||||
EnableConsole: (app.Log.FileLocation == "" || app.Verbosity > 0) && !app.Quiet,
|
|
||||||
FileLocation: app.Log.FileLocation,
|
|
||||||
Level: app.Log.Level,
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.Log.Structured {
|
|
||||||
cfg.Formatter = &logrusUpstream.JSONFormatter{
|
|
||||||
TimestampFormat: "2006-01-02 15:04:05",
|
|
||||||
DisableTimestamp: false,
|
|
||||||
DisableHTMLEscape: false,
|
|
||||||
PrettyPrint: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logWrapper, err := logrus.New(cfg)
|
|
||||||
if err != nil {
|
|
||||||
// this is kinda circular, but we can't return an error... ¯\_(ツ)_/¯
|
|
||||||
// I'm going to leave this here in case we one day have a different default logger other than the "discard" logger
|
|
||||||
log.Error("unable to initialize logger: %+v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
syft.SetLogger(logWrapper)
|
|
||||||
stereoscope.SetLogger(logWrapper.Nested("from-lib", "stereoscope"))
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/bus"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/event"
|
||||||
|
"github.com/anchore/syft/syft/event/monitor"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||||
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] alpine:latest defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry
|
||||||
|
`
|
||||||
|
attestSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp
|
||||||
|
attestHelp = attestExample + attestSchemeHelp
|
||||||
|
cosignBinName = "cosign"
|
||||||
|
)
|
||||||
|
|
||||||
|
type attestOptions struct {
|
||||||
|
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||||
|
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||||
|
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||||
|
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||||
|
options.Attest `yaml:",inline" mapstructure:",squash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Attest(app clio.Application) *cobra.Command {
|
||||||
|
id := app.ID()
|
||||||
|
|
||||||
|
opts := defaultAttestOptions()
|
||||||
|
|
||||||
|
// template format explicitly not allowed
|
||||||
|
opts.Format.Template.Enabled = false
|
||||||
|
|
||||||
|
return app.SetupCommand(&cobra.Command{
|
||||||
|
Use: "attest --output [FORMAT] <IMAGE>",
|
||||||
|
Short: "Generate an SBOM as an attestation for the given [SOURCE] container image",
|
||||||
|
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation that will be uploaded to the image registry",
|
||||||
|
Example: internal.Tprintf(attestHelp, map[string]interface{}{
|
||||||
|
"appName": id.Name,
|
||||||
|
"command": "attest",
|
||||||
|
}),
|
||||||
|
Args: validateScanArgs,
|
||||||
|
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||||
|
defer restoreStdout()
|
||||||
|
|
||||||
|
return runAttest(cmd.Context(), id, &opts, args[0])
|
||||||
|
},
|
||||||
|
}, &opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultAttestOptions() attestOptions {
|
||||||
|
return attestOptions{
|
||||||
|
Output: defaultAttestOutputOptions(),
|
||||||
|
UpdateCheck: options.DefaultUpdateCheck(),
|
||||||
|
Catalog: options.DefaultCatalog(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultAttestOutputOptions() options.Output {
|
||||||
|
return options.Output{
|
||||||
|
AllowMultipleOutputs: false,
|
||||||
|
AllowToFile: false,
|
||||||
|
AllowableOptions: []string{
|
||||||
|
string(syftjson.ID),
|
||||||
|
string(cyclonedxjson.ID),
|
||||||
|
string(spdxjson.ID),
|
||||||
|
string(spdxtagvalue.ID),
|
||||||
|
},
|
||||||
|
Outputs: []string{syftjson.ID.String()},
|
||||||
|
OutputFile: options.OutputFile{ // nolint:staticcheck
|
||||||
|
Enabled: false, // explicitly not allowed
|
||||||
|
},
|
||||||
|
Format: options.DefaultFormat(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:funlen
|
||||||
|
func runAttest(ctx context.Context, id clio.Identification, opts *attestOptions, userInput string) error {
|
||||||
|
// TODO: what other validation here besides binary name?
|
||||||
|
if !commandExists(cosignBinName) {
|
||||||
|
return fmt.Errorf("'syft attest' requires cosign to be installed, however it does not appear to be on PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is the file that will contain the SBOM being attested
|
||||||
|
f, err := os.CreateTemp("", "syft-attest-")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create temp file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
|
s, err := generateSBOMForAttestation(ctx, id, &opts.Catalog, userInput)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to build SBOM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writeSBOMToFormattedFile(s, f, opts); err != nil {
|
||||||
|
return fmt.Errorf("unable to write SBOM to file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = createAttestation(f.Name(), opts, userInput); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.Notify("Attestation has been created, please check your registry for the output or use the cosign command:")
|
||||||
|
bus.Notify(fmt.Sprintf("cosign download attestation %s", userInput))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeSBOMToFormattedFile(s *sbom.SBOM, sbomFile io.Writer, opts *attestOptions) error {
|
||||||
|
if sbomFile == nil {
|
||||||
|
return fmt.Errorf("no output file provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
encs, err := opts.Format.Encoders()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create encoders: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoders := format.NewEncoderCollection(encs...)
|
||||||
|
encoder := encoders.GetByString(opts.Outputs[0])
|
||||||
|
if encoder == nil {
|
||||||
|
return fmt.Errorf("unable to find encoder for %q", opts.Outputs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = encoder.Encode(sbomFile, *s); err != nil {
|
||||||
|
return fmt.Errorf("unable to encode SBOM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAttestation(sbomFilepath string, opts *attestOptions, userInput string) error {
|
||||||
|
execCmd, err := attestCommand(sbomFilepath, opts, userInput)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to craft attest command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation")
|
||||||
|
|
||||||
|
// bus adapter for ui to hook into stdout via an os pipe
|
||||||
|
r, w, err := os.Pipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to create os pipe: %w", err)
|
||||||
|
}
|
||||||
|
defer w.Close()
|
||||||
|
|
||||||
|
mon := progress.NewManual(-1)
|
||||||
|
|
||||||
|
bus.Publish(
|
||||||
|
partybus.Event{
|
||||||
|
Type: event.AttestationStarted,
|
||||||
|
Source: monitor.GenericTask{
|
||||||
|
Title: monitor.Title{
|
||||||
|
Default: "Create attestation",
|
||||||
|
WhileRunning: "Creating attestation",
|
||||||
|
OnSuccess: "Created attestation",
|
||||||
|
},
|
||||||
|
Context: "cosign",
|
||||||
|
},
|
||||||
|
Value: &monitor.ShellProgress{
|
||||||
|
Reader: r,
|
||||||
|
Progressable: mon,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
execCmd.Stdout = w
|
||||||
|
execCmd.Stderr = w
|
||||||
|
|
||||||
|
// attest the SBOM
|
||||||
|
err = execCmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
mon.SetError(err)
|
||||||
|
return fmt.Errorf("unable to attest SBOM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mon.SetCompleted()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func attestCommand(sbomFilepath string, opts *attestOptions, userInput string) (*exec.Cmd, error) {
|
||||||
|
outputNames := opts.OutputNameSet()
|
||||||
|
var outputName string
|
||||||
|
switch outputNames.Size() {
|
||||||
|
case 0:
|
||||||
|
return nil, fmt.Errorf("no output format specified")
|
||||||
|
case 1:
|
||||||
|
outputName = outputNames.List()[0]
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("multiple output formats specified: %s", strings.Join(outputNames.List(), ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{"attest", userInput, "--predicate", sbomFilepath, "--type", predicateType(outputName), "-y"}
|
||||||
|
if opts.Attest.Key != "" {
|
||||||
|
args = append(args, "--key", opts.Attest.Key.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
execCmd := exec.Command(cosignBinName, args...)
|
||||||
|
execCmd.Env = os.Environ()
|
||||||
|
if opts.Attest.Key != "" {
|
||||||
|
execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", opts.Attest.Password))
|
||||||
|
} else {
|
||||||
|
// no key provided, use cosign's keyless mode
|
||||||
|
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
|
||||||
|
}
|
||||||
|
|
||||||
|
return execCmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func predicateType(outputName string) string {
|
||||||
|
// Select Cosign predicate type based on defined output type
|
||||||
|
// As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go
|
||||||
|
switch strings.ToLower(outputName) {
|
||||||
|
case "cyclonedx-json":
|
||||||
|
return "cyclonedx"
|
||||||
|
case "spdx-tag-value", "spdx-tv":
|
||||||
|
return "spdx"
|
||||||
|
case "spdx-json", "json":
|
||||||
|
return "spdxjson"
|
||||||
|
default:
|
||||||
|
return "custom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opts *options.Catalog, userInput string) (*sbom.SBOM, error) {
|
||||||
|
src, err := getSource(opts, userInput, onlyContainerImages)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if src != nil {
|
||||||
|
if err := src.Close(); err != nil {
|
||||||
|
log.Tracef("unable to close source: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s, err := generateSBOM(ctx, id, src, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return nil, fmt.Errorf("no SBOM produced for %q", userInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func onlyContainerImages(d *source.Detection) error {
|
||||||
|
if !d.IsContainerImage() {
|
||||||
|
return fmt.Errorf("attestations are only supported for oci images at this time")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandExists(cmd string) bool {
|
||||||
|
_, err := exec.LookPath(cmd)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_writeSBOMToFormattedFile(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
s *sbom.SBOM
|
||||||
|
opts *attestOptions
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantSbomFile string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "go case",
|
||||||
|
args: args{
|
||||||
|
opts: &attestOptions{
|
||||||
|
Output: func() options.Output {
|
||||||
|
def := defaultAttestOutputOptions()
|
||||||
|
def.Outputs = []string{"syft-json"}
|
||||||
|
return def
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
s: &sbom.SBOM{
|
||||||
|
Artifacts: sbom.Artifacts{},
|
||||||
|
Relationships: nil,
|
||||||
|
Source: source.Description{
|
||||||
|
ID: "source-id",
|
||||||
|
Name: "source-name",
|
||||||
|
Version: "source-version",
|
||||||
|
},
|
||||||
|
Descriptor: sbom.Descriptor{
|
||||||
|
Name: "syft-test",
|
||||||
|
Version: "non-version",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantSbomFile: `{
|
||||||
|
"artifacts": [],
|
||||||
|
"artifactRelationships": [],
|
||||||
|
"source": {
|
||||||
|
"id": "source-id",
|
||||||
|
"name": "source-name",
|
||||||
|
"version": "source-version",
|
||||||
|
"type": "",
|
||||||
|
"metadata": null
|
||||||
|
},
|
||||||
|
"distro": {},
|
||||||
|
"descriptor": {
|
||||||
|
"name": "syft-test",
|
||||||
|
"version": "non-version"
|
||||||
|
},
|
||||||
|
"schema": {}
|
||||||
|
}`,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
sbomFile := &bytes.Buffer{}
|
||||||
|
|
||||||
|
err := writeSBOMToFormattedFile(tt.args.s, sbomFile, tt.args.opts)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("writeSBOMToFormattedFile() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// redact the schema block
|
||||||
|
re := regexp.MustCompile(`(?s)"schema":\W*\{.*?},?`)
|
||||||
|
subject := re.ReplaceAllString(sbomFile.String(), `"schema":{}`)
|
||||||
|
|
||||||
|
assert.JSONEq(t, tt.wantSbomFile, subject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_attestCommand(t *testing.T) {
|
||||||
|
cmdPrefix := cosignBinName
|
||||||
|
lp, err := exec.LookPath(cosignBinName)
|
||||||
|
if err == nil {
|
||||||
|
cmdPrefix = lp
|
||||||
|
}
|
||||||
|
|
||||||
|
fullCmd := func(args string) string {
|
||||||
|
return fmt.Sprintf("%s %s", cmdPrefix, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
sbomFilepath string
|
||||||
|
opts attestOptions
|
||||||
|
userInput string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
wantCmd string
|
||||||
|
wantEnvVars map[string]string
|
||||||
|
notEnvVars []string
|
||||||
|
wantErr require.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "with key and password",
|
||||||
|
args: args{
|
||||||
|
userInput: "myimage",
|
||||||
|
sbomFilepath: "/tmp/sbom-filepath.json",
|
||||||
|
opts: func() attestOptions {
|
||||||
|
def := defaultAttestOptions()
|
||||||
|
def.Outputs = []string{"syft-json"}
|
||||||
|
def.Attest.Key = "key"
|
||||||
|
def.Attest.Password = "password"
|
||||||
|
return def
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
wantCmd: fullCmd("attest myimage --predicate /tmp/sbom-filepath.json --type custom -y --key key"),
|
||||||
|
wantEnvVars: map[string]string{
|
||||||
|
"COSIGN_PASSWORD": "password",
|
||||||
|
},
|
||||||
|
notEnvVars: []string{
|
||||||
|
"COSIGN_EXPERIMENTAL", // only for keyless
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keyless",
|
||||||
|
args: args{
|
||||||
|
userInput: "myimage",
|
||||||
|
sbomFilepath: "/tmp/sbom-filepath.json",
|
||||||
|
opts: func() attestOptions {
|
||||||
|
def := defaultAttestOptions()
|
||||||
|
def.Outputs = []string{"syft-json"}
|
||||||
|
return def
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
wantCmd: fullCmd("attest myimage --predicate /tmp/sbom-filepath.json --type custom -y"),
|
||||||
|
wantEnvVars: map[string]string{
|
||||||
|
"COSIGN_EXPERIMENTAL": "1",
|
||||||
|
},
|
||||||
|
notEnvVars: []string{
|
||||||
|
"COSIGN_PASSWORD",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
tt.wantErr = require.NoError
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := attestCommand(tt.args.sbomFilepath, &tt.args.opts, tt.args.userInput)
|
||||||
|
tt.wantErr(t, err)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotNil(t, got)
|
||||||
|
assert.Equal(t, tt.wantCmd, got.String())
|
||||||
|
|
||||||
|
gotEnv := strset.New(got.Env...)
|
||||||
|
|
||||||
|
for k, v := range tt.wantEnvVars {
|
||||||
|
assert.True(t, gotEnv.Has(fmt.Sprintf("%s=%s", k, v)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, k := range tt.notEnvVars {
|
||||||
|
for _, env := range got.Env {
|
||||||
|
fields := strings.Split(env, "=")
|
||||||
|
if fields[0] == k {
|
||||||
|
t.Errorf("attestCommand() unexpected environment variable %s", k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_predicateType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cyclonedx-json",
|
||||||
|
want: "cyclonedx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdx-tag-value",
|
||||||
|
want: "spdx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdx-tv",
|
||||||
|
want: "spdx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdx-json",
|
||||||
|
want: "spdxjson",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "json",
|
||||||
|
want: "spdxjson",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "syft-json",
|
||||||
|
want: "custom",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equalf(t, tt.want, predicateType(tt.name), "predicateType(%v)", tt.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_buildSBOMForAttestation(t *testing.T) {
|
||||||
|
// note: this test is only meant to test that the filter function is wired
|
||||||
|
// and not the correctness of the function in depth
|
||||||
|
type args struct {
|
||||||
|
id clio.Identification
|
||||||
|
opts *options.Catalog
|
||||||
|
userInput string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *sbom.SBOM
|
||||||
|
wantErr require.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "do not allow directory scans",
|
||||||
|
args: args{
|
||||||
|
opts: func() *options.Catalog {
|
||||||
|
def := defaultAttestOptions()
|
||||||
|
return &def.Catalog
|
||||||
|
}(),
|
||||||
|
userInput: "dir:/tmp/something",
|
||||||
|
},
|
||||||
|
wantErr: require.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
tt.wantErr = require.NoError
|
||||||
|
}
|
||||||
|
_, err := generateSBOMForAttestation(context.Background(), tt.args.id, tt.args.opts, tt.args.userInput)
|
||||||
|
tt.wantErr(t, err)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout
|
||||||
|
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output is written to the file "img.cdx.json""
|
||||||
|
{{.appName}} {{.command}} - -o spdx-json convert an SBOM from STDIN to spdx-json
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConvertOptions struct {
|
||||||
|
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||||
|
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||||
|
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:dupl
|
||||||
|
func Convert(app clio.Application) *cobra.Command {
|
||||||
|
id := app.ID()
|
||||||
|
|
||||||
|
opts := &ConvertOptions{
|
||||||
|
UpdateCheck: options.DefaultUpdateCheck(),
|
||||||
|
Output: options.DefaultOutput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.SetupCommand(&cobra.Command{
|
||||||
|
Use: "convert [SOURCE-SBOM] -o [FORMAT]",
|
||||||
|
Short: "Convert between SBOM formats",
|
||||||
|
Long: "[Experimental] Convert SBOM files to, and from, SPDX, CycloneDX and Syft's format. For more info about data loss between formats see https://github.com/anchore/syft#format-conversion-experimental",
|
||||||
|
Example: internal.Tprintf(convertExample, map[string]interface{}{
|
||||||
|
"appName": id.Name,
|
||||||
|
"command": "convert",
|
||||||
|
}),
|
||||||
|
Args: validateConvertArgs,
|
||||||
|
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||||
|
defer restoreStdout()
|
||||||
|
|
||||||
|
return RunConvert(opts, args[0])
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateConvertArgs(cmd *cobra.Command, args []string) error {
|
||||||
|
return validateArgs(cmd, args, "an SBOM argument is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunConvert(opts *ConvertOptions, userInput string) error {
|
||||||
|
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
||||||
|
|
||||||
|
writer, err := opts.SBOMWriter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader io.ReadSeekCloser
|
||||||
|
|
||||||
|
if userInput == "-" {
|
||||||
|
// though os.Stdin is an os.File, it does not support seeking
|
||||||
|
// you will get errors such as "seek /dev/stdin: illegal seek".
|
||||||
|
// We need to buffer what we read.
|
||||||
|
reader = internal.NewBufferedSeeker(os.Stdin)
|
||||||
|
} else {
|
||||||
|
f, err := os.Open(userInput)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open SBOM file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = f.Close()
|
||||||
|
}()
|
||||||
|
reader = f
|
||||||
|
}
|
||||||
|
|
||||||
|
s, _, _, err := format.Decode(reader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode SBOM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return fmt.Errorf("no SBOM produced")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Write(*s); err != nil {
|
||||||
|
return fmt.Errorf("failed to write SBOM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Packages(app clio.Application, scanCmd *cobra.Command) *cobra.Command {
|
||||||
|
id := app.ID()
|
||||||
|
|
||||||
|
opts := defaultScanOptions()
|
||||||
|
|
||||||
|
cmd := app.SetupCommand(&cobra.Command{
|
||||||
|
Use: "packages [SOURCE]",
|
||||||
|
Short: scanCmd.Short,
|
||||||
|
Long: scanCmd.Long,
|
||||||
|
Args: scanCmd.Args,
|
||||||
|
Example: scanCmd.Example,
|
||||||
|
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||||
|
defer restoreStdout()
|
||||||
|
|
||||||
|
return runScan(cmd.Context(), id, opts, args[0])
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
|
||||||
|
cmd.Deprecated = "use `syft scan` instead"
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_filterExpressionErrors_expressionErrorsHelp(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
wantExpErrs []task.ErrInvalidExpression
|
||||||
|
wantErr assert.ErrorAssertionFunc
|
||||||
|
wantHelp string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no errors",
|
||||||
|
err: nil,
|
||||||
|
wantExpErrs: nil,
|
||||||
|
wantErr: assert.NoError,
|
||||||
|
wantHelp: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single non-expression error is retained",
|
||||||
|
err: errors.New("foo"),
|
||||||
|
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||||
|
return assert.Equal(t, "foo", err.Error())
|
||||||
|
},
|
||||||
|
wantHelp: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple non-expression sibling errors are retained",
|
||||||
|
err: func() error {
|
||||||
|
var err error
|
||||||
|
err = multierror.Append(err, errors.New("foo"))
|
||||||
|
err = multierror.Append(err, errors.New("bar"))
|
||||||
|
return err
|
||||||
|
}(),
|
||||||
|
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||||
|
// note: this is the default formatting from the hashicorp multierror object
|
||||||
|
expected := `2 errors occurred:
|
||||||
|
* foo
|
||||||
|
* bar
|
||||||
|
|
||||||
|
`
|
||||||
|
return assert.Equal(t, expected, err.Error())
|
||||||
|
},
|
||||||
|
wantHelp: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has multiple expression errors (with sibling errors)",
|
||||||
|
err: func() error {
|
||||||
|
var err error
|
||||||
|
err = multierror.Append(err, errors.New("foo"))
|
||||||
|
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed})
|
||||||
|
err = multierror.Append(err, errors.New("bar"))
|
||||||
|
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed})
|
||||||
|
err = multierror.Append(err, errors.New("last"))
|
||||||
|
return err
|
||||||
|
}(),
|
||||||
|
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||||
|
expected := `5 errors occurred:
|
||||||
|
* foo
|
||||||
|
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
|
||||||
|
* bar
|
||||||
|
* invalid expression: "bar": names are not allowed with this operation (must use tags)
|
||||||
|
* last
|
||||||
|
|
||||||
|
`
|
||||||
|
return assert.Equal(t, expected, err.Error())
|
||||||
|
},
|
||||||
|
wantExpErrs: []task.ErrInvalidExpression{
|
||||||
|
{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed},
|
||||||
|
{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed},
|
||||||
|
},
|
||||||
|
wantHelp: `Suggestions:
|
||||||
|
|
||||||
|
❖ Given expression "--select-catalogers foo"
|
||||||
|
However, tags are not allowed with this operation (must use exact names).
|
||||||
|
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||||
|
If you are certain this is what you want to do, use "--override-default-catalogers foo" instead.
|
||||||
|
|
||||||
|
❖ Given expression "--select-catalogers bar"
|
||||||
|
However, names are not allowed with this operation (must use tags).
|
||||||
|
It seems like you are intending to add a cataloger in addition to the default set.
|
||||||
|
... Did you mean "--select-catalogers +bar" instead?
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has multiple expression errors (with error chains and sibling errors)",
|
||||||
|
err: func() error {
|
||||||
|
var err error
|
||||||
|
err = multierror.Append(err, fmt.Errorf("foo: %w", fmt.Errorf("bar: %w", errors.New("last"))))
|
||||||
|
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed})
|
||||||
|
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed})
|
||||||
|
err = multierror.Append(err, errors.New("bottom"))
|
||||||
|
|
||||||
|
return fmt.Errorf("top: %w", fmt.Errorf("middle: %w", err))
|
||||||
|
}(),
|
||||||
|
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||||
|
expected := `top: middle: 4 errors occurred:
|
||||||
|
* foo: bar: last
|
||||||
|
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
|
||||||
|
* invalid expression: "bar": names are not allowed with this operation (must use tags)
|
||||||
|
* bottom
|
||||||
|
|
||||||
|
`
|
||||||
|
return assert.Equal(t, expected, err.Error())
|
||||||
|
},
|
||||||
|
wantExpErrs: []task.ErrInvalidExpression{
|
||||||
|
{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed},
|
||||||
|
{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed},
|
||||||
|
},
|
||||||
|
wantHelp: `Suggestions:
|
||||||
|
|
||||||
|
❖ Given expression "--select-catalogers foo"
|
||||||
|
However, tags are not allowed with this operation (must use exact names).
|
||||||
|
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||||
|
If you are certain this is what you want to do, use "--override-default-catalogers foo" instead.
|
||||||
|
|
||||||
|
❖ Given expression "--select-catalogers bar"
|
||||||
|
However, names are not allowed with this operation (must use tags).
|
||||||
|
It seems like you are intending to add a cataloger in addition to the default set.
|
||||||
|
... Did you mean "--select-catalogers +bar" instead?
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "has multiple expression errors (with error chains and sibling errors)",
|
||||||
|
err: func() error {
|
||||||
|
var err error
|
||||||
|
err = multierror.Append(err, fmt.Errorf("foo: %w", fmt.Errorf("bar: %w", errors.New("last"))))
|
||||||
|
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed})
|
||||||
|
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed})
|
||||||
|
err = multierror.Append(err, errors.New("bottom"))
|
||||||
|
|
||||||
|
// note we wrap the top error in a chain
|
||||||
|
return fmt.Errorf("top: %w", fmt.Errorf("middle: %w", err))
|
||||||
|
}(),
|
||||||
|
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||||
|
expected := `top: middle: 4 errors occurred:
|
||||||
|
* foo: bar: last
|
||||||
|
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
|
||||||
|
* invalid expression: "bar": names are not allowed with this operation (must use tags)
|
||||||
|
* bottom
|
||||||
|
|
||||||
|
`
|
||||||
|
return assert.Equal(t, expected, err.Error())
|
||||||
|
},
|
||||||
|
wantExpErrs: []task.ErrInvalidExpression{
|
||||||
|
{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed},
|
||||||
|
{Expression: "bar", Operation: task.SubSelectOperation, Err: task.ErrNamesNotAllowed},
|
||||||
|
},
|
||||||
|
wantHelp: `Suggestions:
|
||||||
|
|
||||||
|
❖ Given expression "--select-catalogers foo"
|
||||||
|
However, tags are not allowed with this operation (must use exact names).
|
||||||
|
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||||
|
If you are certain this is what you want to do, use "--override-default-catalogers foo" instead.
|
||||||
|
|
||||||
|
❖ Given expression "--select-catalogers bar"
|
||||||
|
However, names are not allowed with this operation (must use tags).
|
||||||
|
It seems like you are intending to add a cataloger in addition to the default set.
|
||||||
|
... Did you mean "--select-catalogers +bar" instead?
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "preserve for any errors within ErrInvalidExpression types",
|
||||||
|
err: func() error {
|
||||||
|
var err error
|
||||||
|
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed})
|
||||||
|
err = multierror.Append(err, task.ErrInvalidExpression{Expression: "bar", Operation: task.SubSelectOperation, Err: errors.New("explanation")}) // this is what makes this test different...
|
||||||
|
|
||||||
|
return err
|
||||||
|
}(),
|
||||||
|
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||||
|
// note: the errors are removed and the help text shows the enriched error help
|
||||||
|
expected := `2 errors occurred:
|
||||||
|
* invalid expression: "foo": tags are not allowed with this operation (must use exact names)
|
||||||
|
* invalid expression: "bar": explanation
|
||||||
|
|
||||||
|
`
|
||||||
|
return assert.Equal(t, expected, err.Error())
|
||||||
|
},
|
||||||
|
wantExpErrs: []task.ErrInvalidExpression{
|
||||||
|
{Expression: "foo", Operation: task.AddOperation, Err: task.ErrTagsNotAllowed},
|
||||||
|
{Expression: "bar", Operation: task.SubSelectOperation, Err: errors.New("explanation")},
|
||||||
|
},
|
||||||
|
wantHelp: `Suggestions:
|
||||||
|
|
||||||
|
❖ Given expression "--select-catalogers foo"
|
||||||
|
However, tags are not allowed with this operation (must use exact names).
|
||||||
|
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||||
|
If you are certain this is what you want to do, use "--override-default-catalogers foo" instead.
|
||||||
|
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotExpErrs := filterExpressionErrors(tt.err)
|
||||||
|
tt.wantErr(t, tt.err) // ensure the error still remains
|
||||||
|
assert.Equal(t, tt.wantExpErrs, gotExpErrs)
|
||||||
|
|
||||||
|
gotHelp := expressionErrorsHelp(gotExpErrs)
|
||||||
|
assert.Equal(t, tt.wantHelp, gotHelp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_expressionSuggestions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
expErr task.ErrInvalidExpression
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no embedded error",
|
||||||
|
expErr: task.ErrInvalidExpression{
|
||||||
|
Expression: "example",
|
||||||
|
},
|
||||||
|
want: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "general error",
|
||||||
|
expErr: task.ErrInvalidExpression{
|
||||||
|
Err: errors.New("general error message"),
|
||||||
|
Expression: "example",
|
||||||
|
},
|
||||||
|
want: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ErrUnknownNameOrTag with add operation",
|
||||||
|
expErr: task.ErrInvalidExpression{
|
||||||
|
Err: task.ErrUnknownNameOrTag,
|
||||||
|
Operation: task.AddOperation,
|
||||||
|
Expression: "+example",
|
||||||
|
},
|
||||||
|
want: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ErrUnknownNameOrTag with subselect operation",
|
||||||
|
expErr: task.ErrInvalidExpression{
|
||||||
|
Err: task.ErrUnknownNameOrTag,
|
||||||
|
Operation: task.SubSelectOperation,
|
||||||
|
Expression: "example",
|
||||||
|
},
|
||||||
|
want: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ErrNamesNotAllowed with subselect operator",
|
||||||
|
expErr: task.ErrInvalidExpression{
|
||||||
|
Err: task.ErrNamesNotAllowed,
|
||||||
|
Operation: task.SubSelectOperation,
|
||||||
|
Expression: "example",
|
||||||
|
},
|
||||||
|
want: ` ❖ Given expression "--select-catalogers example"
|
||||||
|
However, names are not allowed with this operation (must use tags).
|
||||||
|
It seems like you are intending to add a cataloger in addition to the default set.
|
||||||
|
... Did you mean "--select-catalogers +example" instead?
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ErrTagsNotAllowed with add operation",
|
||||||
|
expErr: task.ErrInvalidExpression{
|
||||||
|
Err: task.ErrTagsNotAllowed,
|
||||||
|
Operation: task.AddOperation,
|
||||||
|
Expression: "+example",
|
||||||
|
},
|
||||||
|
want: ` ❖ Given expression "--select-catalogers +example"
|
||||||
|
However, tags are not allowed with this operation (must use exact names).
|
||||||
|
Adding groups of catalogers may result in surprising behavior (create inaccurate SBOMs).
|
||||||
|
If you are certain this is what you want to do, use "--override-default-catalogers example" instead.
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ErrAllNotAllowed with subselect operation",
|
||||||
|
expErr: task.ErrInvalidExpression{
|
||||||
|
Err: task.ErrAllNotAllowed,
|
||||||
|
Operation: task.SubSelectOperation,
|
||||||
|
Expression: "example",
|
||||||
|
},
|
||||||
|
want: ` ❖ Given expression "--select-catalogers example"
|
||||||
|
However, you cannot use the 'all' operand in this context.
|
||||||
|
It seems like you are intending to use all catalogers (which is not recommended).
|
||||||
|
... Did you mean "--override-default-catalogers example" instead?
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, expressionSuggetions(tt.expErr))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Root(app clio.Application, packagesCmd *cobra.Command) *cobra.Command {
|
||||||
|
id := app.ID()
|
||||||
|
|
||||||
|
opts := defaultScanOptions()
|
||||||
|
|
||||||
|
return app.SetupRootCommand(&cobra.Command{
|
||||||
|
Use: fmt.Sprintf("%s [SOURCE]", app.ID().Name),
|
||||||
|
Short: packagesCmd.Short,
|
||||||
|
Long: packagesCmd.Long,
|
||||||
|
Args: packagesCmd.Args,
|
||||||
|
Example: packagesCmd.Example,
|
||||||
|
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||||
|
defer restoreStdout()
|
||||||
|
|
||||||
|
return runScan(cmd.Context(), id, opts, args[0])
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
@@ -0,0 +1,447 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal/ui"
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/bus"
|
||||||
|
"github.com/anchore/syft/internal/file"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/internal/task"
|
||||||
|
"github.com/anchore/syft/syft"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
scanExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
|
||||||
|
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
|
||||||
|
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX formatted SBOM
|
||||||
|
{{.appName}} {{.command}} alpine:latest -o cyclonedx-json show a CycloneDX JSON formatted SBOM
|
||||||
|
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.3 Tag-Value formatted SBOM
|
||||||
|
{{.appName}} {{.command}} alpine:latest -o spdx@2.2 show a SPDX 2.2 Tag-Value formatted SBOM
|
||||||
|
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.3 JSON formatted SBOM
|
||||||
|
{{.appName}} {{.command}} alpine:latest -o spdx-json@2.2 show a SPDX 2.2 JSON formatted SBOM
|
||||||
|
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||||
|
{{.appName}} {{.command}} alpine:latest -o template -t my_format.tmpl show a SBOM formatted according to given template file
|
||||||
|
|
||||||
|
Supports the following image sources:
|
||||||
|
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
|
||||||
|
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory
|
||||||
|
`
|
||||||
|
|
||||||
|
schemeHelpHeader = "You can also explicitly specify the scheme to use:"
|
||||||
|
imageSchemeHelp = ` {{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
||||||
|
{{.appName}} {{.command}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon
|
||||||
|
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
|
||||||
|
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
|
||||||
|
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
|
||||||
|
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
|
||||||
|
{{.appName}} {{.command}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk
|
||||||
|
`
|
||||||
|
nonImageSchemeHelp = ` {{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
|
||||||
|
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
|
||||||
|
`
|
||||||
|
scanSchemeHelp = "\n " + schemeHelpHeader + "\n" + imageSchemeHelp + nonImageSchemeHelp
|
||||||
|
|
||||||
|
scanHelp = scanExample + scanSchemeHelp
|
||||||
|
)
|
||||||
|
|
||||||
|
type scanOptions struct {
|
||||||
|
options.Config `yaml:",inline" mapstructure:",squash"`
|
||||||
|
options.Output `yaml:",inline" mapstructure:",squash"`
|
||||||
|
options.UpdateCheck `yaml:",inline" mapstructure:",squash"`
|
||||||
|
options.Catalog `yaml:",inline" mapstructure:",squash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultScanOptions() *scanOptions {
|
||||||
|
return &scanOptions{
|
||||||
|
Output: options.DefaultOutput(),
|
||||||
|
UpdateCheck: options.DefaultUpdateCheck(),
|
||||||
|
Catalog: options.DefaultCatalog(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:dupl
|
||||||
|
func Scan(app clio.Application) *cobra.Command {
|
||||||
|
id := app.ID()
|
||||||
|
|
||||||
|
opts := defaultScanOptions()
|
||||||
|
|
||||||
|
return app.SetupCommand(&cobra.Command{
|
||||||
|
Use: "scan [SOURCE]",
|
||||||
|
Short: "Generate an SBOM",
|
||||||
|
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
|
||||||
|
Example: internal.Tprintf(scanHelp, map[string]interface{}{
|
||||||
|
"appName": id.Name,
|
||||||
|
"command": "scan",
|
||||||
|
}),
|
||||||
|
Args: validateScanArgs,
|
||||||
|
PreRunE: applicationUpdateCheck(id, &opts.UpdateCheck),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
restoreStdout := ui.CaptureStdoutToTraceLog()
|
||||||
|
defer restoreStdout()
|
||||||
|
|
||||||
|
return runScan(cmd.Context(), id, opts, args[0])
|
||||||
|
},
|
||||||
|
}, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *scanOptions) PostLoad() error {
|
||||||
|
return o.validateLegacyOptionsNotUsed()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *scanOptions) validateLegacyOptionsNotUsed() error {
|
||||||
|
if o.Config.ConfigFile == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for legacy config file shapes that are no longer valid
|
||||||
|
type legacyConfig struct {
|
||||||
|
BasePath *string `yaml:"base-path" json:"base-path" mapstructure:"base-path"`
|
||||||
|
DefaultImagePullSource *string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"`
|
||||||
|
ExcludeBinaryOverlapByOwnership *bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"`
|
||||||
|
File any `yaml:"file" json:"file" mapstructure:"file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
by, err := os.ReadFile(o.Config.ConfigFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read config file during validations %q: %w", o.Config.ConfigFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var legacy legacyConfig
|
||||||
|
if err := yaml.Unmarshal(by, &legacy); err != nil {
|
||||||
|
return fmt.Errorf("unable to parse config file during validations %q: %w", o.Config.ConfigFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if legacy.DefaultImagePullSource != nil {
|
||||||
|
return fmt.Errorf("the config file option 'default-image-pull-source' has been removed, please use 'source.image.default-pull-source' instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
if legacy.ExcludeBinaryOverlapByOwnership != nil {
|
||||||
|
return fmt.Errorf("the config file option 'exclude-binary-overlap-by-ownership' has been removed, please use 'relationships.exclude-binary-packages-with-file-ownership-overlap' instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
if legacy.BasePath != nil {
|
||||||
|
return fmt.Errorf("the config file option 'base-path' has been removed, please use 'source.base-path' instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
if legacy.File != nil && reflect.TypeOf(legacy.File).Kind() == reflect.String {
|
||||||
|
return fmt.Errorf("the config file option 'file' has been removed, please use 'outputs' instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateScanArgs(cmd *cobra.Command, args []string) error {
|
||||||
|
return validateArgs(cmd, args, "an image/directory argument is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateArgs(cmd *cobra.Command, args []string, error string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
|
||||||
|
if err := cmd.Help(); err != nil {
|
||||||
|
return fmt.Errorf("unable to display help: %w", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cobra.MaximumNArgs(1)(cmd, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:funlen
|
||||||
|
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
|
||||||
|
writer, err := opts.SBOMWriter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := getSource(&opts.Catalog, userInput)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if src != nil {
|
||||||
|
if err := src.Close(); err != nil {
|
||||||
|
log.Tracef("unable to close source: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s, err := generateSBOM(ctx, id, src, &opts.Catalog)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s == nil {
|
||||||
|
return fmt.Errorf("no SBOM produced for %q", userInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writer.Write(*s); err != nil {
|
||||||
|
return fmt.Errorf("failed to write SBOM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSource(opts *options.Catalog, userInput string, filters ...func(*source.Detection) error) (source.Source, error) {
|
||||||
|
detection, err := source.Detect(
|
||||||
|
userInput,
|
||||||
|
source.DetectConfig{
|
||||||
|
DefaultImageSource: opts.Source.Image.DefaultPullSource,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not deteremine source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filter := range filters {
|
||||||
|
if err := filter(detection); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var platform *image.Platform
|
||||||
|
|
||||||
|
if opts.Platform != "" {
|
||||||
|
platform, err = image.NewPlatform(opts.Platform)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid platform: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hashers, err := file.Hashers(opts.Source.File.Digests...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid hash algorithm: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
src, err := detection.NewSource(
|
||||||
|
source.DetectionSourceConfig{
|
||||||
|
Alias: source.Alias{
|
||||||
|
Name: opts.Source.Name,
|
||||||
|
Version: opts.Source.Version,
|
||||||
|
},
|
||||||
|
RegistryOptions: opts.Registry.ToOptions(),
|
||||||
|
Platform: platform,
|
||||||
|
Exclude: source.ExcludeConfig{
|
||||||
|
Paths: opts.Exclusions,
|
||||||
|
},
|
||||||
|
DigestAlgorithms: hashers,
|
||||||
|
BasePath: opts.Source.BasePath,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if userInput == "power-user" {
|
||||||
|
bus.Notify("Note: the 'power-user' command has been removed.")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to construct source from user input %q: %w", userInput, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return src, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateSBOM(ctx context.Context, id clio.Identification, src source.Source, opts *options.Catalog) (*sbom.SBOM, error) {
|
||||||
|
s, err := syft.CreateSBOM(ctx, src, opts.ToSBOMConfig(id))
|
||||||
|
if err != nil {
|
||||||
|
expErrs := filterExpressionErrors(err)
|
||||||
|
notifyExpressionErrors(expErrs)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterExpressionErrors(err error) []task.ErrInvalidExpression {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expErrs := processErrors(err)
|
||||||
|
|
||||||
|
return expErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// processErrors traverses error chains and multierror lists and returns all ErrInvalidExpression errors found
|
||||||
|
func processErrors(err error) []task.ErrInvalidExpression {
|
||||||
|
var result []task.ErrInvalidExpression
|
||||||
|
|
||||||
|
var processError func(...error)
|
||||||
|
processError = func(errs ...error) {
|
||||||
|
for _, e := range errs {
|
||||||
|
// note: using errors.As will result in surprising behavior (since that will traverse the error chain,
|
||||||
|
// potentially skipping over nodes in a list of errors)
|
||||||
|
if cerr, ok := e.(task.ErrInvalidExpression); ok {
|
||||||
|
result = append(result, cerr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var multiErr *multierror.Error
|
||||||
|
if errors.As(e, &multiErr) {
|
||||||
|
processError(multiErr.Errors...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processError(err)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func notifyExpressionErrors(expErrs []task.ErrInvalidExpression) {
|
||||||
|
helpText := expressionErrorsHelp(expErrs)
|
||||||
|
if helpText == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bus.Notify(helpText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expressionErrorsHelp(expErrs []task.ErrInvalidExpression) string {
|
||||||
|
// enrich all errors found with CLI hints
|
||||||
|
if len(expErrs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sb := strings.Builder{}
|
||||||
|
|
||||||
|
sb.WriteString("Suggestions:\n\n")
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for i, expErr := range expErrs {
|
||||||
|
help := expressionSuggetions(expErr)
|
||||||
|
if help == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
sb.WriteString(help)
|
||||||
|
if i != len(expErrs)-1 {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
const expressionHelpTemplate = " ❖ Given expression %q\n%s%s"
|
||||||
|
|
||||||
|
func expressionSuggetions(expErr task.ErrInvalidExpression) string {
|
||||||
|
if expErr.Err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
hint := getHintPhrase(expErr)
|
||||||
|
if hint == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(expressionHelpTemplate,
|
||||||
|
getExpression(expErr),
|
||||||
|
indentMsg(getExplanation(expErr)),
|
||||||
|
indentMsg(hint),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func indentMsg(msg string) string {
|
||||||
|
if msg == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(msg, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
lines[i] = " " + line
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(lines, "\n") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExpression(expErr task.ErrInvalidExpression) string {
|
||||||
|
flag := "--select-catalogers"
|
||||||
|
if expErr.Operation == task.SetOperation {
|
||||||
|
flag = "--override-default-catalogers"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s %s", flag, expErr.Expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExplanation(expErr task.ErrInvalidExpression) string {
|
||||||
|
err := expErr.Err
|
||||||
|
if errors.Is(err, task.ErrUnknownNameOrTag) {
|
||||||
|
noun := ""
|
||||||
|
switch expErr.Operation {
|
||||||
|
case task.AddOperation:
|
||||||
|
noun = "name"
|
||||||
|
case task.SubSelectOperation:
|
||||||
|
noun = "tag"
|
||||||
|
default:
|
||||||
|
noun = "name or tag"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("However, %q is not a recognized cataloger %s.", trimOperation(expErr.Expression), noun)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, task.ErrNamesNotAllowed) {
|
||||||
|
if expErr.Operation == task.SubSelectOperation {
|
||||||
|
return "However, " + err.Error() + ".\nIt seems like you are intending to add a cataloger in addition to the default set." // nolint:goconst
|
||||||
|
}
|
||||||
|
return "However, " + err.Error() + "." // nolint:goconst
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, task.ErrTagsNotAllowed) {
|
||||||
|
return "However, " + err.Error() + ".\nAdding groups of catalogers may result in surprising behavior (create inaccurate SBOMs)." // nolint:goconst
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, task.ErrAllNotAllowed) {
|
||||||
|
return "However, you " + err.Error() + ".\nIt seems like you are intending to use all catalogers (which is not recommended)."
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "However, this is not valid: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHintPhrase(expErr task.ErrInvalidExpression) string {
|
||||||
|
if errors.Is(expErr.Err, task.ErrUnknownNameOrTag) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch expErr.Operation {
|
||||||
|
case task.AddOperation:
|
||||||
|
if errors.Is(expErr.Err, task.ErrTagsNotAllowed) {
|
||||||
|
return fmt.Sprintf("If you are certain this is what you want to do, use %q instead.", "--override-default-catalogers "+trimOperation(expErr.Expression))
|
||||||
|
}
|
||||||
|
|
||||||
|
case task.SubSelectOperation:
|
||||||
|
didYouMean := "... Did you mean %q instead?"
|
||||||
|
if errors.Is(expErr.Err, task.ErrNamesNotAllowed) {
|
||||||
|
return fmt.Sprintf(didYouMean, "--select-catalogers +"+expErr.Expression)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(expErr.Err, task.ErrAllNotAllowed) {
|
||||||
|
return fmt.Sprintf(didYouMean, "--override-default-catalogers "+expErr.Expression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimOperation(x string) string {
|
||||||
|
return strings.TrimLeft(x, "+-")
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_scanOptions_validateLegacyOptionsNotUsed(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cfg string
|
||||||
|
wantErr assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no config file",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config file with no legacy options",
|
||||||
|
cfg: "test-fixtures/scan-configs/no-legacy-options.yaml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config file with default image pull source legacy option",
|
||||||
|
cfg: "test-fixtures/scan-configs/with-default-pull-source.yaml",
|
||||||
|
wantErr: assertErrorContains("source.image.default-pull-source"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config file with exclude-binary-overlap-by-ownership legacy option",
|
||||||
|
cfg: "test-fixtures/scan-configs/with-exclude-binary-overlap-by-ownership.yaml",
|
||||||
|
wantErr: assertErrorContains("relationships.exclude-binary-packages-with-file-ownership-overlap"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config file with file string legacy option",
|
||||||
|
cfg: "test-fixtures/scan-configs/with-file-string.yaml",
|
||||||
|
wantErr: assertErrorContains("outputs"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config file with file section",
|
||||||
|
cfg: "test-fixtures/scan-configs/with-file-section.yaml",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "config file with base-path legacy option",
|
||||||
|
cfg: "test-fixtures/scan-configs/with-base-path.yaml",
|
||||||
|
wantErr: assertErrorContains("source.base-path"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
tt.wantErr = assert.NoError
|
||||||
|
}
|
||||||
|
o := &scanOptions{
|
||||||
|
Config: options.Config{ConfigFile: tt.cfg},
|
||||||
|
}
|
||||||
|
tt.wantErr(t, o.validateLegacyOptionsNotUsed())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertErrorContains(contains string) assert.ErrorAssertionFunc {
|
||||||
|
return func(t assert.TestingT, err error, i ...interface{}) bool {
|
||||||
|
return assert.ErrorContains(t, err, contains, i...)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# simple, valid syft config with no legacy fields used
|
||||||
|
parallelism: 5
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
base-path: "something"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
default-image-pull-source: "something"
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
exclude-binary-overlap-by-ownership: true
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
file:
|
||||||
|
selection: all
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
file: path
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/wagoodman/go-partybus"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
hashiVersion "github.com/anchore/go-version"
|
||||||
|
"github.com/anchore/syft/cmd/syft/cli/options"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/bus"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/event"
|
||||||
|
"github.com/anchore/syft/syft/event/parsers"
|
||||||
|
)
|
||||||
|
|
||||||
|
var latestAppVersionURL = struct {
|
||||||
|
host string
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
host: "https://toolbox-data.anchore.io",
|
||||||
|
path: "/syft/releases/latest/VERSION",
|
||||||
|
}
|
||||||
|
|
||||||
|
func applicationUpdateCheck(id clio.Identification, check *options.UpdateCheck) func(cmd *cobra.Command, args []string) error {
|
||||||
|
return func(cmd *cobra.Command, args []string) error {
|
||||||
|
if check.CheckForAppUpdate {
|
||||||
|
checkForApplicationUpdate(id)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForApplicationUpdate(id clio.Identification) {
|
||||||
|
log.Debugf("checking if a new version of %s is available", id.Name)
|
||||||
|
isAvailable, newVersion, err := isUpdateAvailable(id)
|
||||||
|
if err != nil {
|
||||||
|
// this should never stop the application
|
||||||
|
log.Errorf(err.Error())
|
||||||
|
}
|
||||||
|
if isAvailable {
|
||||||
|
log.Infof("new version of %s is available: %s (current version is %s)", id.Name, newVersion, id.Version)
|
||||||
|
|
||||||
|
bus.Publish(partybus.Event{
|
||||||
|
Type: event.CLIAppUpdateAvailable,
|
||||||
|
Value: parsers.UpdateCheck{
|
||||||
|
New: newVersion,
|
||||||
|
Current: id.Version,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log.Debugf("no new %s update available", id.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is.
|
||||||
|
func isUpdateAvailable(id clio.Identification) (bool, string, error) {
|
||||||
|
if !isProductionBuild(id.Version) {
|
||||||
|
// don't allow for non-production builds to check for a version.
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
currentVersion, err := hashiVersion.NewVersion(id.Version)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", fmt.Errorf("failed to parse current application version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
latestVersion, err := fetchLatestApplicationVersion(id)
|
||||||
|
if err != nil {
|
||||||
|
return false, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestVersion.GreaterThan(currentVersion) {
|
||||||
|
return true, latestVersion.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isProductionBuild(version string) bool {
|
||||||
|
if strings.Contains(version, "SNAPSHOT") || strings.Contains(version, internal.NotProvided) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchLatestApplicationVersion(id clio.Identification) (*hashiVersion.Version, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request for latest version: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Add("User-Agent", fmt.Sprintf("%v %v", id.Name, id.Version))
|
||||||
|
|
||||||
|
client := http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch latest version: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read latest version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionStr := strings.TrimSuffix(string(versionBytes), "\n")
|
||||||
|
if len(versionStr) > 50 {
|
||||||
|
return nil, fmt.Errorf("version too long: %q", versionStr[:50])
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashiVersion.NewVersion(versionStr)
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package version
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
hashiVersion "github.com/anchore/go-version"
|
hashiVersion "github.com/anchore/go-version"
|
||||||
|
"github.com/anchore/syft/cmd/syft/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsUpdateAvailable(t *testing.T) {
|
func TestIsUpdateAvailable(t *testing.T) {
|
||||||
@@ -74,7 +76,7 @@ func TestIsUpdateAvailable(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "NoBuildVersion",
|
name: "NoBuildVersion",
|
||||||
buildVersion: valueNotProvided,
|
buildVersion: internal.NotProvided,
|
||||||
latestVersion: "1.0.0",
|
latestVersion: "1.0.0",
|
||||||
code: 200,
|
code: 200,
|
||||||
isAvailable: false,
|
isAvailable: false,
|
||||||
@@ -105,7 +107,7 @@ func TestIsUpdateAvailable(t *testing.T) {
|
|||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
// setup mocks
|
// setup mocks
|
||||||
// local...
|
// local...
|
||||||
version = test.buildVersion
|
id := clio.Identification{Name: "Syft", Version: test.buildVersion}
|
||||||
// remote...
|
// remote...
|
||||||
handler := http.NewServeMux()
|
handler := http.NewServeMux()
|
||||||
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
|
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -116,7 +118,7 @@ func TestIsUpdateAvailable(t *testing.T) {
|
|||||||
latestAppVersionURL.host = mockSrv.URL
|
latestAppVersionURL.host = mockSrv.URL
|
||||||
defer mockSrv.Close()
|
defer mockSrv.Close()
|
||||||
|
|
||||||
isAvailable, newVersion, err := IsUpdateAvailable()
|
isAvailable, newVersion, err := isUpdateAvailable(id)
|
||||||
if err != nil && !test.err {
|
if err != nil && !test.err {
|
||||||
t.Fatalf("got error but expected none: %+v", err)
|
t.Fatalf("got error but expected none: %+v", err)
|
||||||
} else if err == nil && test.err {
|
} else if err == nil && test.err {
|
||||||
@@ -137,52 +139,67 @@ func TestIsUpdateAvailable(t *testing.T) {
|
|||||||
|
|
||||||
func TestFetchLatestApplicationVersion(t *testing.T) {
|
func TestFetchLatestApplicationVersion(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
response string
|
response string
|
||||||
code int
|
code int
|
||||||
err bool
|
err bool
|
||||||
expected *hashiVersion.Version
|
id clio.Identification
|
||||||
|
expected *hashiVersion.Version
|
||||||
|
expectedHeaders map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "gocase",
|
name: "gocase",
|
||||||
response: "1.0.0",
|
response: "1.0.0",
|
||||||
code: 200,
|
code: 200,
|
||||||
expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")),
|
id: clio.Identification{Name: "Syft", Version: "0.0.0"},
|
||||||
|
expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")),
|
||||||
|
expectedHeaders: map[string]string{"User-Agent": "Syft 0.0.0"},
|
||||||
|
err: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "garbage",
|
name: "garbage",
|
||||||
response: "garbage",
|
response: "garbage",
|
||||||
code: 200,
|
code: 200,
|
||||||
expected: nil,
|
id: clio.Identification{Name: "Syft", Version: "0.0.0"},
|
||||||
err: true,
|
expected: nil,
|
||||||
|
expectedHeaders: nil,
|
||||||
|
err: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "http 500",
|
name: "http 500",
|
||||||
response: "1.0.0",
|
response: "1.0.0",
|
||||||
code: 500,
|
code: 500,
|
||||||
expected: nil,
|
id: clio.Identification{Name: "Syft", Version: "0.0.0"},
|
||||||
err: true,
|
expected: nil,
|
||||||
|
expectedHeaders: nil,
|
||||||
|
err: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "http 404",
|
name: "http 404",
|
||||||
response: "1.0.0",
|
response: "1.0.0",
|
||||||
code: 404,
|
code: 404,
|
||||||
expected: nil,
|
id: clio.Identification{Name: "Syft", Version: "0.0.0"},
|
||||||
err: true,
|
expected: nil,
|
||||||
|
expectedHeaders: nil,
|
||||||
|
err: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
response: "",
|
response: "",
|
||||||
code: 200,
|
code: 200,
|
||||||
expected: nil,
|
id: clio.Identification{Name: "Syft", Version: "0.0.0"},
|
||||||
err: true,
|
expected: nil,
|
||||||
|
expectedHeaders: nil,
|
||||||
|
err: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "too long",
|
name: "too long",
|
||||||
response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ",
|
response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ",
|
||||||
code: 200,
|
code: 200,
|
||||||
expected: nil,
|
id: clio.Identification{Name: "Syft", Version: "0.0.0"},
|
||||||
err: true,
|
expected: nil,
|
||||||
|
expectedHeaders: nil,
|
||||||
|
err: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +208,15 @@ func TestFetchLatestApplicationVersion(t *testing.T) {
|
|||||||
// setup mock
|
// setup mock
|
||||||
handler := http.NewServeMux()
|
handler := http.NewServeMux()
|
||||||
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
|
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if test.expectedHeaders != nil {
|
||||||
|
for headerName, headerValue := range test.expectedHeaders {
|
||||||
|
actualHeader := r.Header.Get(headerName)
|
||||||
|
if actualHeader != headerValue {
|
||||||
|
t.Fatalf("expected header %v=%v but got %v", headerName, headerValue, actualHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
w.WriteHeader(test.code)
|
w.WriteHeader(test.code)
|
||||||
_, _ = w.Write([]byte(test.response))
|
_, _ = w.Write([]byte(test.response))
|
||||||
})
|
})
|
||||||
@@ -198,7 +224,7 @@ func TestFetchLatestApplicationVersion(t *testing.T) {
|
|||||||
latestAppVersionURL.host = mockSrv.URL
|
latestAppVersionURL.host = mockSrv.URL
|
||||||
defer mockSrv.Close()
|
defer mockSrv.Close()
|
||||||
|
|
||||||
actual, err := fetchLatestApplicationVersion()
|
actual, err := fetchLatestApplicationVersion(test.id)
|
||||||
if err != nil && !test.err {
|
if err != nil && !test.err {
|
||||||
t.Fatalf("got error but expected none: %+v", err)
|
t.Fatalf("got error but expected none: %+v", err)
|
||||||
} else if err == nil && test.err {
|
} else if err == nil && test.err {
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Completion() *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "completion [bash|zsh|fish]",
|
|
||||||
Short: "Generate a shell completion for Syft (listing local docker images)",
|
|
||||||
Long: `To load completions (docker image list):
|
|
||||||
Bash:
|
|
||||||
$ source <(syft completion bash)
|
|
||||||
# To load completions for each session, execute once:
|
|
||||||
Linux:
|
|
||||||
$ syft completion bash > /etc/bash_completion.d/syft
|
|
||||||
MacOS:
|
|
||||||
$ syft completion bash > /usr/local/etc/bash_completion.d/syft
|
|
||||||
Zsh:
|
|
||||||
# If shell completion is not already enabled in your environment you will need
|
|
||||||
# to enable it. You can execute the following once:
|
|
||||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
|
||||||
# To load completions for each session, execute once:
|
|
||||||
$ syft completion zsh > "${fpath[1]}/_syft"
|
|
||||||
# You will need to start a new shell for this setup to take effect.
|
|
||||||
Fish:
|
|
||||||
$ syft completion fish | source
|
|
||||||
# To load completions for each session, execute once:
|
|
||||||
$ syft completion fish > ~/.config/fish/completions/syft.fish
|
|
||||||
`,
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
ValidArgs: []string{"bash", "zsh", "fish"},
|
|
||||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
var err error
|
|
||||||
switch args[0] {
|
|
||||||
case "bash":
|
|
||||||
err = cmd.Root().GenBashCompletion(os.Stdout)
|
|
||||||
case "zsh":
|
|
||||||
err = cmd.Root().GenZshCompletion(os.Stdout)
|
|
||||||
case "fish":
|
|
||||||
err = cmd.Root().GenFishCompletion(os.Stdout, true)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package cli
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/cmd/syft/cli/convert"
|
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
"github.com/anchore/syft/internal/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
convertExample = ` {{.appName}} {{.command}} img.syft.json -o spdx-json convert a syft SBOM to spdx-json, output goes to stdout in table format, by default
|
|
||||||
{{.appName}} {{.command}} img.syft.json -o cyclonedx-json=img.cdx.json convert a syft SBOM to CycloneDX, output goes to a file named img.cdx.json
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:dupl
|
|
||||||
func Convert(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command {
|
|
||||||
cmd := &cobra.Command{
|
|
||||||
Use: "convert [SOURCE-SBOM] -o [FORMAT]",
|
|
||||||
Short: "Convert between SBOM formats",
|
|
||||||
Long: "[Experimental] Convert SBOM files to, and from, SPDX, CycloneDX and Syft's format. For more info about data loss between formats see https://github.com/anchore/syft#format-conversion-experimental",
|
|
||||||
Example: internal.Tprintf(convertExample, map[string]interface{}{
|
|
||||||
"appName": internal.ApplicationName,
|
|
||||||
"command": "convert",
|
|
||||||
}),
|
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if err := app.LoadAllValues(v, ro.Config); err != nil {
|
|
||||||
return fmt.Errorf("invalid application config: %w", err)
|
|
||||||
}
|
|
||||||
newLogWrapper(app)
|
|
||||||
logApplicationConfig(app)
|
|
||||||
return validateArgs(cmd, args)
|
|
||||||
},
|
|
||||||
SilenceUsage: true,
|
|
||||||
SilenceErrors: true,
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
if app.CheckForAppUpdate {
|
|
||||||
checkForApplicationUpdate()
|
|
||||||
}
|
|
||||||
return convert.Run(cmd.Context(), app, args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := po.AddFlags(cmd, v)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package convert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/cmd/syft/cli/options"
|
|
||||||
"github.com/anchore/syft/internal/config"
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
"github.com/anchore/syft/syft"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Run(ctx context.Context, app *config.Application, args []string) error {
|
|
||||||
log.Warn("convert is an experimental feature, run `syft convert -h` for help")
|
|
||||||
writer, err := options.MakeWriter(app.Outputs, app.File, "")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if err := writer.Close(); err != nil {
|
|
||||||
log.Warnf("unable to write to report destination: %w", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// this can only be a SBOM file
|
|
||||||
userInput := args[0]
|
|
||||||
f, err := os.Open(userInput)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open SBOM file: %w", err)
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
sbom, _, err := syft.Decode(f)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to decode SBOM: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return writer.Write(*sbom)
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
package eventloop
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
"github.com/anchore/syft/internal/ui"
|
|
||||||
)
|
|
||||||
|
|
||||||
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
|
|
||||||
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
|
|
||||||
// an eventual graceful exit.
|
|
||||||
func EventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
|
|
||||||
defer cleanupFn()
|
|
||||||
events := subscription.Events()
|
|
||||||
var err error
|
|
||||||
var ux ui.UI
|
|
||||||
|
|
||||||
if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var retErr error
|
|
||||||
var forceTeardown bool
|
|
||||||
|
|
||||||
for {
|
|
||||||
if workerErrs == nil && events == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case err, isOpen := <-workerErrs:
|
|
||||||
if !isOpen {
|
|
||||||
workerErrs = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
// capture the error from the worker and unsubscribe to complete a graceful shutdown
|
|
||||||
retErr = multierror.Append(retErr, err)
|
|
||||||
_ = subscription.Unsubscribe()
|
|
||||||
// the worker has exited, we may have been mid-handling events for the UI which should now be
|
|
||||||
// ignored, in which case forcing a teardown of the UI irregardless of the state is required.
|
|
||||||
forceTeardown = true
|
|
||||||
}
|
|
||||||
case e, isOpen := <-events:
|
|
||||||
if !isOpen {
|
|
||||||
events = nil
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ux.Handle(e); err != nil {
|
|
||||||
if errors.Is(err, partybus.ErrUnsubscribe) {
|
|
||||||
events = nil
|
|
||||||
} else {
|
|
||||||
retErr = multierror.Append(retErr, err)
|
|
||||||
// TODO: should we unsubscribe? should we try to halt execution? or continue?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case <-signals:
|
|
||||||
// ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up.
|
|
||||||
// we ignore further errors since cleaning up the tmp directories will affect running catalogers that are
|
|
||||||
// reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result.
|
|
||||||
|
|
||||||
// TODO: potential future improvement would be to pass context into workers with a cancel function that is
|
|
||||||
// to the event loop. In this way we can have a more controlled shutdown even at the most nested levels
|
|
||||||
// of processing.
|
|
||||||
events = nil
|
|
||||||
workerErrs = nil
|
|
||||||
forceTeardown = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ux.Teardown(forceTeardown); err != nil {
|
|
||||||
retErr = multierror.Append(retErr, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return retErr
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use
|
|
||||||
// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error
|
|
||||||
// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks
|
|
||||||
// when there are environmental problem (e.g. unable to setup a TUI with the current TTY).
|
|
||||||
func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) {
|
|
||||||
for _, ux := range uis {
|
|
||||||
if err := ux.Setup(unsubscribe); err != nil {
|
|
||||||
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return ux, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("unable to setup any UI")
|
|
||||||
}
|
|
||||||
@@ -1,456 +0,0 @@
|
|||||||
package eventloop
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/mock"
|
|
||||||
"github.com/wagoodman/go-partybus"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/ui"
|
|
||||||
"github.com/anchore/syft/syft/event"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ ui.UI = (*uiMock)(nil)
|
|
||||||
|
|
||||||
type uiMock struct {
|
|
||||||
t *testing.T
|
|
||||||
finalEvent partybus.Event
|
|
||||||
unsubscribe func() error
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *uiMock) Setup(unsubscribe func() error) error {
|
|
||||||
u.t.Logf("UI Setup called")
|
|
||||||
u.unsubscribe = unsubscribe
|
|
||||||
return u.Called(unsubscribe).Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *uiMock) Handle(event partybus.Event) error {
|
|
||||||
u.t.Logf("UI Handle called: %+v", event.Type)
|
|
||||||
if event == u.finalEvent {
|
|
||||||
assert.NoError(u.t, u.unsubscribe())
|
|
||||||
}
|
|
||||||
return u.Called(event).Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *uiMock) Teardown(_ bool) error {
|
|
||||||
u.t.Logf("UI Teardown called")
|
|
||||||
return u.Called().Error(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_EventLoop_gracefulExit(t *testing.T) {
|
|
||||||
test := func(t *testing.T) {
|
|
||||||
|
|
||||||
testBus := partybus.NewBus()
|
|
||||||
subscription := testBus.Subscribe()
|
|
||||||
t.Cleanup(testBus.Close)
|
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
|
||||||
Type: event.Exit,
|
|
||||||
}
|
|
||||||
|
|
||||||
worker := func() <-chan error {
|
|
||||||
ret := make(chan error)
|
|
||||||
go func() {
|
|
||||||
t.Log("worker running")
|
|
||||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
|
||||||
// then close (a partial shutdown).
|
|
||||||
ret <- nil
|
|
||||||
t.Log("worker sent nothing")
|
|
||||||
close(ret)
|
|
||||||
t.Log("worker closed")
|
|
||||||
// do the other half of the shutdown
|
|
||||||
testBus.Publish(finalEvent)
|
|
||||||
t.Log("worker published final event")
|
|
||||||
}()
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
signaler := func() <-chan os.Signal {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ux := &uiMock{
|
|
||||||
t: t,
|
|
||||||
finalEvent: finalEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the mock sees at least the final event
|
|
||||||
ux.On("Handle", finalEvent).Return(nil)
|
|
||||||
// ensure the mock sees basic setup/teardown events
|
|
||||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
|
||||||
ux.On("Teardown").Return(nil)
|
|
||||||
|
|
||||||
var cleanupCalled bool
|
|
||||||
cleanupFn := func() {
|
|
||||||
t.Log("cleanup called")
|
|
||||||
cleanupCalled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.NoError(t,
|
|
||||||
EventLoop(
|
|
||||||
worker(),
|
|
||||||
signaler(),
|
|
||||||
subscription,
|
|
||||||
cleanupFn,
|
|
||||||
ux,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
|
||||||
ux.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a bug, then there is a risk of the event loop never returning
|
|
||||||
testWithTimeout(t, 5*time.Second, test)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_EventLoop_workerError(t *testing.T) {
|
|
||||||
test := func(t *testing.T) {
|
|
||||||
|
|
||||||
testBus := partybus.NewBus()
|
|
||||||
subscription := testBus.Subscribe()
|
|
||||||
t.Cleanup(testBus.Close)
|
|
||||||
|
|
||||||
workerErr := fmt.Errorf("worker error")
|
|
||||||
|
|
||||||
worker := func() <-chan error {
|
|
||||||
ret := make(chan error)
|
|
||||||
go func() {
|
|
||||||
t.Log("worker running")
|
|
||||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
|
||||||
// then close (a partial shutdown).
|
|
||||||
ret <- nil
|
|
||||||
t.Log("worker sent nothing")
|
|
||||||
ret <- workerErr
|
|
||||||
t.Log("worker sent error")
|
|
||||||
close(ret)
|
|
||||||
t.Log("worker closed")
|
|
||||||
// note: NO final event is fired
|
|
||||||
}()
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
signaler := func() <-chan os.Signal {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ux := &uiMock{
|
|
||||||
t: t,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the mock sees basic setup/teardown events
|
|
||||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
|
||||||
ux.On("Teardown").Return(nil)
|
|
||||||
|
|
||||||
var cleanupCalled bool
|
|
||||||
cleanupFn := func() {
|
|
||||||
t.Log("cleanup called")
|
|
||||||
cleanupCalled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure we see an error returned
|
|
||||||
assert.ErrorIs(t,
|
|
||||||
EventLoop(
|
|
||||||
worker(),
|
|
||||||
signaler(),
|
|
||||||
subscription,
|
|
||||||
cleanupFn,
|
|
||||||
ux,
|
|
||||||
),
|
|
||||||
workerErr,
|
|
||||||
"should have seen a worker error, but did not",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
|
||||||
ux.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a bug, then there is a risk of the event loop never returning
|
|
||||||
testWithTimeout(t, 5*time.Second, test)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_EventLoop_unsubscribeError(t *testing.T) {
|
|
||||||
test := func(t *testing.T) {
|
|
||||||
|
|
||||||
testBus := partybus.NewBus()
|
|
||||||
subscription := testBus.Subscribe()
|
|
||||||
t.Cleanup(testBus.Close)
|
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
|
||||||
Type: event.Exit,
|
|
||||||
}
|
|
||||||
|
|
||||||
worker := func() <-chan error {
|
|
||||||
ret := make(chan error)
|
|
||||||
go func() {
|
|
||||||
t.Log("worker running")
|
|
||||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
|
||||||
// then close (a partial shutdown).
|
|
||||||
ret <- nil
|
|
||||||
t.Log("worker sent nothing")
|
|
||||||
close(ret)
|
|
||||||
t.Log("worker closed")
|
|
||||||
// do the other half of the shutdown
|
|
||||||
testBus.Publish(finalEvent)
|
|
||||||
t.Log("worker published final event")
|
|
||||||
}()
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
signaler := func() <-chan os.Signal {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ux := &uiMock{
|
|
||||||
t: t,
|
|
||||||
finalEvent: finalEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the mock sees at least the final event... note the unsubscribe error here
|
|
||||||
ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe)
|
|
||||||
// ensure the mock sees basic setup/teardown events
|
|
||||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
|
||||||
ux.On("Teardown").Return(nil)
|
|
||||||
|
|
||||||
var cleanupCalled bool
|
|
||||||
cleanupFn := func() {
|
|
||||||
t.Log("cleanup called")
|
|
||||||
cleanupCalled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that
|
|
||||||
// this case is handled as a controlled shutdown (this test should not timeout)
|
|
||||||
assert.NoError(t,
|
|
||||||
EventLoop(
|
|
||||||
worker(),
|
|
||||||
signaler(),
|
|
||||||
subscription,
|
|
||||||
cleanupFn,
|
|
||||||
ux,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
|
||||||
ux.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a bug, then there is a risk of the event loop never returning
|
|
||||||
testWithTimeout(t, 5*time.Second, test)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_EventLoop_handlerError(t *testing.T) {
|
|
||||||
test := func(t *testing.T) {
|
|
||||||
|
|
||||||
testBus := partybus.NewBus()
|
|
||||||
subscription := testBus.Subscribe()
|
|
||||||
t.Cleanup(testBus.Close)
|
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
|
||||||
Type: event.Exit,
|
|
||||||
Error: fmt.Errorf("an exit error occured"),
|
|
||||||
}
|
|
||||||
|
|
||||||
worker := func() <-chan error {
|
|
||||||
ret := make(chan error)
|
|
||||||
go func() {
|
|
||||||
t.Log("worker running")
|
|
||||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
|
||||||
// then close (a partial shutdown).
|
|
||||||
ret <- nil
|
|
||||||
t.Log("worker sent nothing")
|
|
||||||
close(ret)
|
|
||||||
t.Log("worker closed")
|
|
||||||
// do the other half of the shutdown
|
|
||||||
testBus.Publish(finalEvent)
|
|
||||||
t.Log("worker published final event")
|
|
||||||
}()
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
signaler := func() <-chan os.Signal {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ux := &uiMock{
|
|
||||||
t: t,
|
|
||||||
finalEvent: finalEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the mock sees at least the final event... note the event error is propagated
|
|
||||||
ux.On("Handle", finalEvent).Return(finalEvent.Error)
|
|
||||||
// ensure the mock sees basic setup/teardown events
|
|
||||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
|
||||||
ux.On("Teardown").Return(nil)
|
|
||||||
|
|
||||||
var cleanupCalled bool
|
|
||||||
cleanupFn := func() {
|
|
||||||
t.Log("cleanup called")
|
|
||||||
cleanupCalled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle errors SHOULD propagate the event loop. We are additionally asserting that this case is
|
|
||||||
// handled as a controlled shutdown (this test should not timeout)
|
|
||||||
assert.ErrorIs(t,
|
|
||||||
EventLoop(
|
|
||||||
worker(),
|
|
||||||
signaler(),
|
|
||||||
subscription,
|
|
||||||
cleanupFn,
|
|
||||||
ux,
|
|
||||||
),
|
|
||||||
finalEvent.Error,
|
|
||||||
"should have seen a event error, but did not",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
|
||||||
ux.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a bug, then there is a risk of the event loop never returning
|
|
||||||
testWithTimeout(t, 5*time.Second, test)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_EventLoop_signalsStopExecution(t *testing.T) {
|
|
||||||
test := func(t *testing.T) {
|
|
||||||
|
|
||||||
testBus := partybus.NewBus()
|
|
||||||
subscription := testBus.Subscribe()
|
|
||||||
t.Cleanup(testBus.Close)
|
|
||||||
|
|
||||||
worker := func() <-chan error {
|
|
||||||
// the worker will never return work and the event loop will always be waiting...
|
|
||||||
return make(chan error)
|
|
||||||
}
|
|
||||||
|
|
||||||
signaler := func() <-chan os.Signal {
|
|
||||||
ret := make(chan os.Signal)
|
|
||||||
go func() {
|
|
||||||
ret <- syscall.SIGINT
|
|
||||||
// note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit
|
|
||||||
}()
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
ux := &uiMock{
|
|
||||||
t: t,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the mock sees basic setup/teardown events
|
|
||||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
|
||||||
ux.On("Teardown").Return(nil)
|
|
||||||
|
|
||||||
var cleanupCalled bool
|
|
||||||
cleanupFn := func() {
|
|
||||||
t.Log("cleanup called")
|
|
||||||
cleanupCalled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.NoError(t,
|
|
||||||
EventLoop(
|
|
||||||
worker(),
|
|
||||||
signaler(),
|
|
||||||
subscription,
|
|
||||||
cleanupFn,
|
|
||||||
ux,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
|
||||||
ux.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a bug, then there is a risk of the event loop never returning
|
|
||||||
testWithTimeout(t, 5*time.Second, test)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_EventLoop_uiTeardownError(t *testing.T) {
|
|
||||||
test := func(t *testing.T) {
|
|
||||||
|
|
||||||
testBus := partybus.NewBus()
|
|
||||||
subscription := testBus.Subscribe()
|
|
||||||
t.Cleanup(testBus.Close)
|
|
||||||
|
|
||||||
finalEvent := partybus.Event{
|
|
||||||
Type: event.Exit,
|
|
||||||
}
|
|
||||||
|
|
||||||
worker := func() <-chan error {
|
|
||||||
ret := make(chan error)
|
|
||||||
go func() {
|
|
||||||
t.Log("worker running")
|
|
||||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
|
||||||
// then close (a partial shutdown).
|
|
||||||
ret <- nil
|
|
||||||
t.Log("worker sent nothing")
|
|
||||||
close(ret)
|
|
||||||
t.Log("worker closed")
|
|
||||||
// do the other half of the shutdown
|
|
||||||
testBus.Publish(finalEvent)
|
|
||||||
t.Log("worker published final event")
|
|
||||||
}()
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
signaler := func() <-chan os.Signal {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ux := &uiMock{
|
|
||||||
t: t,
|
|
||||||
finalEvent: finalEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down")
|
|
||||||
|
|
||||||
// ensure the mock sees at least the final event... note the event error is propagated
|
|
||||||
ux.On("Handle", finalEvent).Return(nil)
|
|
||||||
// ensure the mock sees basic setup/teardown events
|
|
||||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
|
||||||
ux.On("Teardown").Return(teardownError)
|
|
||||||
|
|
||||||
var cleanupCalled bool
|
|
||||||
cleanupFn := func() {
|
|
||||||
t.Log("cleanup called")
|
|
||||||
cleanupCalled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure we see an error returned
|
|
||||||
assert.ErrorIs(t,
|
|
||||||
EventLoop(
|
|
||||||
worker(),
|
|
||||||
signaler(),
|
|
||||||
subscription,
|
|
||||||
cleanupFn,
|
|
||||||
ux,
|
|
||||||
),
|
|
||||||
teardownError,
|
|
||||||
"should have seen a UI teardown error, but did not",
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
|
||||||
ux.AssertExpectations(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there is a bug, then there is a risk of the event loop never returning
|
|
||||||
testWithTimeout(t, 5*time.Second, test)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
|
|
||||||
done := make(chan bool)
|
|
||||||
go func() {
|
|
||||||
test(t)
|
|
||||||
done <- true
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(timeout):
|
|
||||||
t.Fatal("test timed out")
|
|
||||||
case <-done:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
package eventloop
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SetupSignals() <-chan os.Signal {
|
|
||||||
c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify
|
|
||||||
|
|
||||||
interruptions := []os.Signal{
|
|
||||||
syscall.SIGINT,
|
|
||||||
syscall.SIGTERM,
|
|
||||||
}
|
|
||||||
|
|
||||||
signal.Notify(c, interruptions...)
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
package eventloop
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/config"
|
|
||||||
"github.com/anchore/syft/syft"
|
|
||||||
"github.com/anchore/syft/syft/artifact"
|
|
||||||
"github.com/anchore/syft/syft/file"
|
|
||||||
"github.com/anchore/syft/syft/sbom"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
|
|
||||||
|
|
||||||
func Tasks(app *config.Application) ([]Task, error) {
|
|
||||||
var tasks []Task
|
|
||||||
|
|
||||||
generators := []func(app *config.Application) (Task, error){
|
|
||||||
generateCatalogPackagesTask,
|
|
||||||
generateCatalogFileMetadataTask,
|
|
||||||
generateCatalogFileDigestsTask,
|
|
||||||
generateCatalogSecretsTask,
|
|
||||||
generateCatalogContentsTask,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, generator := range generators {
|
|
||||||
task, err := generator(app)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if task != nil {
|
|
||||||
tasks = append(tasks, task)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tasks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateCatalogPackagesTask(app *config.Application) (Task, error) {
|
|
||||||
if !app.Package.Cataloger.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
|
||||||
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, app.ToCatalogerConfig())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results.PackageCatalog = packageCatalog
|
|
||||||
results.LinuxDistribution = theDistro
|
|
||||||
|
|
||||||
return relationships, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return task, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateCatalogFileMetadataTask(app *config.Application) (Task, error) {
|
|
||||||
if !app.FileMetadata.Cataloger.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
metadataCataloger := file.NewMetadataCataloger()
|
|
||||||
|
|
||||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
|
||||||
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := metadataCataloger.Catalog(resolver)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
results.FileMetadata = result
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return task, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateCatalogFileDigestsTask(app *config.Application) (Task, error) {
|
|
||||||
if !app.FileMetadata.Cataloger.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
supportedHashAlgorithms := make(map[string]crypto.Hash)
|
|
||||||
for _, h := range []crypto.Hash{
|
|
||||||
crypto.MD5,
|
|
||||||
crypto.SHA1,
|
|
||||||
crypto.SHA256,
|
|
||||||
} {
|
|
||||||
supportedHashAlgorithms[file.DigestAlgorithmName(h)] = h
|
|
||||||
}
|
|
||||||
|
|
||||||
var hashes []crypto.Hash
|
|
||||||
for _, hashStr := range app.FileMetadata.Digests {
|
|
||||||
name := file.CleanDigestAlgorithmName(hashStr)
|
|
||||||
hashObj, ok := supportedHashAlgorithms[name]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("unsupported hash algorithm: %s", hashStr)
|
|
||||||
}
|
|
||||||
hashes = append(hashes, hashObj)
|
|
||||||
}
|
|
||||||
|
|
||||||
digestsCataloger, err := file.NewDigestsCataloger(hashes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
|
||||||
resolver, err := src.FileResolver(app.FileMetadata.Cataloger.ScopeOpt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := digestsCataloger.Catalog(resolver)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
results.FileDigests = result
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return task, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateCatalogSecretsTask(app *config.Application) (Task, error) {
|
|
||||||
if !app.Secrets.Cataloger.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
patterns, err := file.GenerateSearchPatterns(file.DefaultSecretsPatterns, app.Secrets.AdditionalPatterns, app.Secrets.ExcludePatternNames)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
secretsCataloger, err := file.NewSecretsCataloger(patterns, app.Secrets.RevealValues, app.Secrets.SkipFilesAboveSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
|
||||||
resolver, err := src.FileResolver(app.Secrets.Cataloger.ScopeOpt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := secretsCataloger.Catalog(resolver)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
results.Secrets = result
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return task, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateCatalogContentsTask(app *config.Application) (Task, error) {
|
|
||||||
if !app.FileContents.Cataloger.Enabled {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
contentsCataloger, err := file.NewContentsCataloger(app.FileContents.Globs, app.FileContents.SkipFilesAboveSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
|
|
||||||
resolver, err := src.FileResolver(app.FileContents.Cataloger.ScopeOpt)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := contentsCataloger.Catalog(resolver)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
results.FileContents = result
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return task, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunTask(t Task, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
|
|
||||||
defer close(c)
|
|
||||||
|
|
||||||
relationships, err := t(a, src)
|
|
||||||
if err != nil {
|
|
||||||
errs <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, relationship := range relationships {
|
|
||||||
c <- relationship
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +1,17 @@
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/anchore/clio"
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultKeyFileName = "cosign.key"
|
type Attest struct {
|
||||||
|
// IMPORTANT: do not show the attestation key/password in any YAML/JSON output (sensitive information)
|
||||||
type AttestOptions struct {
|
Key secret `yaml:"key" json:"key" mapstructure:"key"`
|
||||||
Key string
|
Password secret `yaml:"password" json:"password" mapstructure:"password"`
|
||||||
Cert string
|
|
||||||
CertChain string
|
|
||||||
NoUpload bool
|
|
||||||
Force bool
|
|
||||||
Recursive bool
|
|
||||||
|
|
||||||
Rekor RekorOptions
|
|
||||||
Fulcio FulcioOptions
|
|
||||||
OIDC OIDCOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Interface = (*AttestOptions)(nil)
|
var _ clio.FlagAdder = (*Attest)(nil)
|
||||||
|
|
||||||
func (o *AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
func (o Attest) AddFlags(flags clio.FlagSet) {
|
||||||
if err := o.Rekor.AddFlags(cmd, v); err != nil {
|
flags.StringVarP((*string)(&o.Key), "key", "k", "the key to use for the attestation")
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := o.Fulcio.AddFlags(cmd, v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := o.OIDC.AddFlags(cmd, v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&o.Key, "key", "", defaultKeyFileName,
|
|
||||||
"path to the private key file to use for attestation")
|
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&o.Cert, "cert", "", "",
|
|
||||||
"path to the x.509 certificate in PEM format to include in the OCI Signature")
|
|
||||||
|
|
||||||
cmd.Flags().BoolVarP(&o.NoUpload, "no-upload", "", false,
|
|
||||||
"do not upload the generated attestation")
|
|
||||||
|
|
||||||
cmd.Flags().BoolVarP(&o.Force, "force", "", false,
|
|
||||||
"skip warnings and confirmations")
|
|
||||||
|
|
||||||
cmd.Flags().BoolVarP(&o.Recursive, "recursive", "", false,
|
|
||||||
"if a multi-arch image is specified, additionally sign each discrete image")
|
|
||||||
|
|
||||||
return bindAttestationConfigOptions(cmd.Flags(), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindAttestationConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
|
||||||
if err := v.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("attest.cert", flags.Lookup("cert")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("attest.no-upload", flags.Lookup("no-upload")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("attest.force", flags.Lookup("force")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("attest.recursive", flags.Lookup("recursive")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/iancoleman/strcase"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/fangs"
|
||||||
|
intFile "github.com/anchore/syft/internal/file"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft"
|
||||||
|
"github.com/anchore/syft/syft/cataloging"
|
||||||
|
"github.com/anchore/syft/syft/cataloging/filecataloging"
|
||||||
|
"github.com/anchore/syft/syft/cataloging/pkgcataloging"
|
||||||
|
"github.com/anchore/syft/syft/file/cataloger/filecontent"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/binary"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/golang"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/java"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/kernel"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/python"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Catalog struct {
|
||||||
|
// high-level cataloger configuration
|
||||||
|
Catalogers []string `yaml:"-" json:"catalogers" mapstructure:"catalogers"` // deprecated and not shown in yaml output
|
||||||
|
DefaultCatalogers []string `yaml:"default-catalogers" json:"default-catalogers" mapstructure:"default-catalogers"`
|
||||||
|
SelectCatalogers []string `yaml:"select-catalogers" json:"select-catalogers" mapstructure:"select-catalogers"`
|
||||||
|
Package packageConfig `yaml:"package" json:"package" mapstructure:"package"`
|
||||||
|
File fileConfig `yaml:"file" json:"file" mapstructure:"file"`
|
||||||
|
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
|
||||||
|
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
|
||||||
|
Relationships relationshipsConfig `yaml:"relationships" json:"relationships" mapstructure:"relationships"`
|
||||||
|
|
||||||
|
// ecosystem-specific cataloger configuration
|
||||||
|
Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
|
||||||
|
Java javaConfig `yaml:"java" json:"java" mapstructure:"java"`
|
||||||
|
JavaScript javaScriptConfig `yaml:"javascript" json:"javascript" mapstructure:"javascript"`
|
||||||
|
LinuxKernel linuxKernelConfig `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"`
|
||||||
|
Python pythonConfig `yaml:"python" json:"python" mapstructure:"python"`
|
||||||
|
|
||||||
|
// configuration for the source (the subject being analyzed)
|
||||||
|
Registry registryConfig `yaml:"registry" json:"registry" mapstructure:"registry"`
|
||||||
|
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
|
||||||
|
Name string `yaml:"name" json:"name" mapstructure:"name"` // deprecated
|
||||||
|
Source sourceConfig `yaml:"source" json:"source" mapstructure:"source"`
|
||||||
|
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
clio.FlagAdder
|
||||||
|
clio.PostLoader
|
||||||
|
fangs.FieldDescriber
|
||||||
|
} = (*Catalog)(nil)
|
||||||
|
|
||||||
|
func DefaultCatalog() Catalog {
|
||||||
|
return Catalog{
|
||||||
|
Scope: source.SquashedScope.String(),
|
||||||
|
Package: defaultPackageConfig(),
|
||||||
|
LinuxKernel: defaultLinuxKernelConfig(),
|
||||||
|
File: defaultFileConfig(),
|
||||||
|
Relationships: defaultRelationshipsConfig(),
|
||||||
|
Source: defaultSourceConfig(),
|
||||||
|
Parallelism: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Catalog) ToSBOMConfig(id clio.Identification) *syft.CreateSBOMConfig {
|
||||||
|
return syft.DefaultCreateSBOMConfig().
|
||||||
|
WithTool(id.Name, id.Version).
|
||||||
|
WithParallelism(cfg.Parallelism).
|
||||||
|
WithRelationshipsConfig(cfg.ToRelationshipsConfig()).
|
||||||
|
WithSearchConfig(cfg.ToSearchConfig()).
|
||||||
|
WithPackagesConfig(cfg.ToPackagesConfig()).
|
||||||
|
WithFilesConfig(cfg.ToFilesConfig()).
|
||||||
|
WithCatalogerSelection(
|
||||||
|
pkgcataloging.NewSelectionRequest().
|
||||||
|
WithDefaults(cfg.DefaultCatalogers...).
|
||||||
|
WithExpression(cfg.SelectCatalogers...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Catalog) ToSearchConfig() cataloging.SearchConfig {
|
||||||
|
return cataloging.SearchConfig{
|
||||||
|
Scope: source.ParseScope(cfg.Scope),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Catalog) ToRelationshipsConfig() cataloging.RelationshipsConfig {
|
||||||
|
return cataloging.RelationshipsConfig{
|
||||||
|
PackageFileOwnership: cfg.Relationships.PackageFileOwnership,
|
||||||
|
PackageFileOwnershipOverlap: cfg.Relationships.PackageFileOwnershipOverlap,
|
||||||
|
// note: this option was surfaced in the syft application configuration before this relationships section was added
|
||||||
|
ExcludeBinaryPackagesWithFileOwnershipOverlap: cfg.Package.ExcludeBinaryOverlapByOwnership,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Catalog) ToFilesConfig() filecataloging.Config {
|
||||||
|
hashers, err := intFile.Hashers(cfg.File.Metadata.Digests...)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warn("unable to configure file hashers")
|
||||||
|
}
|
||||||
|
|
||||||
|
return filecataloging.Config{
|
||||||
|
Selection: cfg.File.Metadata.Selection,
|
||||||
|
Hashers: hashers,
|
||||||
|
Content: filecontent.Config{
|
||||||
|
Globs: cfg.File.Content.Globs,
|
||||||
|
SkipFilesAboveSize: cfg.File.Content.SkipFilesAboveSize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
|
||||||
|
archiveSearch := cataloging.ArchiveSearchConfig{
|
||||||
|
IncludeIndexedArchives: cfg.Package.SearchIndexedArchives,
|
||||||
|
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
|
||||||
|
}
|
||||||
|
return pkgcataloging.Config{
|
||||||
|
Binary: binary.DefaultCatalogerConfig(),
|
||||||
|
Golang: golang.DefaultCatalogerConfig().
|
||||||
|
WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses).
|
||||||
|
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
|
||||||
|
WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses).
|
||||||
|
WithProxy(cfg.Golang.Proxy).
|
||||||
|
WithNoProxy(cfg.Golang.NoProxy),
|
||||||
|
JavaScript: javascript.DefaultCatalogerConfig().
|
||||||
|
WithSearchRemoteLicenses(cfg.JavaScript.SearchRemoteLicenses).
|
||||||
|
WithNpmBaseURL(cfg.JavaScript.NpmBaseURL),
|
||||||
|
LinuxKernel: kernel.LinuxKernelCatalogerConfig{
|
||||||
|
CatalogModules: cfg.LinuxKernel.CatalogModules,
|
||||||
|
},
|
||||||
|
Python: python.CatalogerConfig{
|
||||||
|
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
|
||||||
|
},
|
||||||
|
JavaArchive: java.DefaultArchiveCatalogerConfig().
|
||||||
|
WithUseNetwork(cfg.Java.UseNetwork).
|
||||||
|
WithMavenBaseURL(cfg.Java.MavenURL).
|
||||||
|
WithArchiveTraversal(archiveSearch, cfg.Java.MaxParentRecursiveDepth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Catalog) AddFlags(flags clio.FlagSet) {
|
||||||
|
var validScopeValues []string
|
||||||
|
for _, scope := range source.AllScopes {
|
||||||
|
validScopeValues = append(validScopeValues, strcase.ToDelimited(string(scope), '-'))
|
||||||
|
}
|
||||||
|
flags.StringVarP(&cfg.Scope, "scope", "s",
|
||||||
|
fmt.Sprintf("selection of layers to catalog, options=%v", validScopeValues))
|
||||||
|
|
||||||
|
flags.StringVarP(&cfg.Platform, "platform", "",
|
||||||
|
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
|
||||||
|
|
||||||
|
flags.StringArrayVarP(&cfg.Exclusions, "exclude", "",
|
||||||
|
"exclude paths from being scanned using a glob expression")
|
||||||
|
|
||||||
|
flags.StringArrayVarP(&cfg.Catalogers, "catalogers", "",
|
||||||
|
"enable one or more package catalogers")
|
||||||
|
|
||||||
|
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||||
|
if err := pfp.PFlagSet().MarkDeprecated("catalogers", "use: override-default-catalogers and select-catalogers"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic("unable to mark flags as deprecated")
|
||||||
|
}
|
||||||
|
|
||||||
|
flags.StringArrayVarP(&cfg.DefaultCatalogers, "override-default-catalogers", "",
|
||||||
|
"set the base set of catalogers to use (defaults to 'image' or 'directory' depending on the scan source)")
|
||||||
|
|
||||||
|
flags.StringArrayVarP(&cfg.SelectCatalogers, "select-catalogers", "",
|
||||||
|
"add, remove, and filter the catalogers to be used")
|
||||||
|
|
||||||
|
flags.StringVarP(&cfg.Source.Name, "name", "",
|
||||||
|
"set the name of the target being analyzed")
|
||||||
|
|
||||||
|
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||||
|
flagSet := pfp.PFlagSet()
|
||||||
|
flagSet.Lookup("name").Deprecated = "use: source-name"
|
||||||
|
}
|
||||||
|
|
||||||
|
flags.StringVarP(&cfg.Source.Name, "source-name", "",
|
||||||
|
"set the name of the target being analyzed")
|
||||||
|
|
||||||
|
flags.StringVarP(&cfg.Source.Version, "source-version", "",
|
||||||
|
"set the version of the target being analyzed")
|
||||||
|
|
||||||
|
flags.StringVarP(&cfg.Source.BasePath, "base-path", "",
|
||||||
|
"base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Catalog) DescribeFields(descriptions fangs.FieldDescriptionSet) {
|
||||||
|
descriptions.Add(&cfg.Parallelism, "number of cataloger workers to run in parallel")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Catalog) PostLoad() error {
|
||||||
|
if cfg.Name != "" {
|
||||||
|
log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version")
|
||||||
|
if cfg.Source.Name == "" {
|
||||||
|
cfg.Source.Name = cfg.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usingLegacyCatalogers := len(cfg.Catalogers) > 0
|
||||||
|
usingNewCatalogers := len(cfg.DefaultCatalogers) > 0 || len(cfg.SelectCatalogers) > 0
|
||||||
|
|
||||||
|
if usingLegacyCatalogers && usingNewCatalogers {
|
||||||
|
return fmt.Errorf("cannot use both 'catalogers' and 'select-catalogers'/'default-catalogers' flags")
|
||||||
|
}
|
||||||
|
|
||||||
|
flatten := func(l []string) []string {
|
||||||
|
var out []string
|
||||||
|
for _, v := range l {
|
||||||
|
for _, s := range strings.Split(v, ",") {
|
||||||
|
out = append(out, strings.TrimSpace(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Catalogers = flatten(cfg.Catalogers)
|
||||||
|
cfg.DefaultCatalogers = flatten(cfg.DefaultCatalogers)
|
||||||
|
cfg.SelectCatalogers = flatten(cfg.SelectCatalogers)
|
||||||
|
|
||||||
|
// for backwards compatibility
|
||||||
|
cfg.DefaultCatalogers = append(cfg.DefaultCatalogers, cfg.Catalogers...)
|
||||||
|
|
||||||
|
s := source.ParseScope(cfg.Scope)
|
||||||
|
if s == source.UnknownScope {
|
||||||
|
return fmt.Errorf("bad scope value %q", cfg.Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCatalog_PostLoad(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
options Catalog
|
||||||
|
assert func(t *testing.T, options Catalog)
|
||||||
|
wantErr assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "mutually exclusive cataloger flags (cat / def-cat)",
|
||||||
|
options: Catalog{
|
||||||
|
Catalogers: []string{"foo,bar", "42"},
|
||||||
|
DefaultCatalogers: []string{"some,thing"},
|
||||||
|
Scope: "squashed",
|
||||||
|
},
|
||||||
|
wantErr: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mutually exclusive cataloger flags (cat / sel-cat)",
|
||||||
|
options: Catalog{
|
||||||
|
Catalogers: []string{"foo,bar", "42"},
|
||||||
|
SelectCatalogers: []string{"some,thing"},
|
||||||
|
Scope: "squashed",
|
||||||
|
},
|
||||||
|
wantErr: assert.Error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allow old cataloger flags",
|
||||||
|
options: Catalog{
|
||||||
|
Catalogers: []string{"foo,bar"},
|
||||||
|
Scope: "squashed",
|
||||||
|
},
|
||||||
|
assert: func(t *testing.T, options Catalog) {
|
||||||
|
assert.Equal(t, []string{"bar", "foo"}, options.DefaultCatalogers) // note: sorted order
|
||||||
|
assert.Equal(t, []string{"bar", "foo"}, options.Catalogers) // note: sorted order
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "allow new cataloger flags",
|
||||||
|
options: Catalog{
|
||||||
|
SelectCatalogers: []string{"foo,bar", "42"},
|
||||||
|
DefaultCatalogers: []string{"some,thing"},
|
||||||
|
Scope: "squashed",
|
||||||
|
},
|
||||||
|
assert: func(t *testing.T, options Catalog) {
|
||||||
|
assert.Equal(t, []string{"42", "bar", "foo"}, options.SelectCatalogers) // note: sorted order
|
||||||
|
assert.Equal(t, []string{"some", "thing"}, options.DefaultCatalogers) // note: sorted order
|
||||||
|
assert.Empty(t, options.Catalogers)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.wantErr == nil {
|
||||||
|
tt.wantErr = assert.NoError
|
||||||
|
}
|
||||||
|
tt.wantErr(t, tt.options.PostLoad(), fmt.Sprintf("PostLoad()"))
|
||||||
|
if tt.assert != nil {
|
||||||
|
tt.assert(t, tt.options)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import "github.com/anchore/fangs"
|
||||||
|
|
||||||
|
// Config holds a reference to the specific config file that was used to load application configuration
|
||||||
|
type Config struct {
|
||||||
|
ConfigFile string `yaml:"config" json:"config" mapstructure:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) DescribeFields(descriptions fangs.FieldDescriptionSet) {
|
||||||
|
descriptions.Add(&cfg.ConfigFile, "the configuration file that was used to load application configuration")
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
intFile "github.com/anchore/syft/internal/file"
|
||||||
|
"github.com/anchore/syft/syft/file"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileConfig struct {
|
||||||
|
Metadata fileMetadata `yaml:"metadata" json:"metadata" mapstructure:"metadata"`
|
||||||
|
Content fileContent `yaml:"content" json:"content" mapstructure:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileMetadata struct {
|
||||||
|
Selection file.Selection `yaml:"selection" json:"selection" mapstructure:"selection"`
|
||||||
|
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileContent struct {
|
||||||
|
SkipFilesAboveSize int64 `yaml:"skip-files-above-size" json:"skip-files-above-size" mapstructure:"skip-files-above-size"`
|
||||||
|
Globs []string `yaml:"globs" json:"globs" mapstructure:"globs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultFileConfig() fileConfig {
|
||||||
|
return fileConfig{
|
||||||
|
Metadata: fileMetadata{
|
||||||
|
Selection: file.FilesOwnedByPackageSelection,
|
||||||
|
Digests: []string{"sha1", "sha256"},
|
||||||
|
},
|
||||||
|
Content: fileContent{
|
||||||
|
SkipFilesAboveSize: 250 * intFile.KB,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fileConfig) PostLoad() error {
|
||||||
|
switch c.Metadata.Selection {
|
||||||
|
case file.NoFilesSelection, file.FilesOwnedByPackageSelection, file.AllFilesSelection:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("invalid file metadata selection: %q", c.Metadata.Selection)
|
||||||
|
}
|
||||||
+120
-30
@@ -1,39 +1,129 @@
|
|||||||
package options
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/anchore/syft/syft/formats/cyclonedxjson"
|
"fmt"
|
||||||
"github.com/anchore/syft/syft/formats/cyclonedxxml"
|
|
||||||
"github.com/anchore/syft/syft/formats/github"
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/anchore/syft/syft/formats/spdxjson"
|
|
||||||
"github.com/anchore/syft/syft/formats/spdxtagvalue"
|
"github.com/anchore/clio"
|
||||||
"github.com/anchore/syft/syft/formats/syftjson"
|
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||||
"github.com/anchore/syft/syft/formats/table"
|
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||||
"github.com/anchore/syft/syft/formats/text"
|
"github.com/anchore/syft/syft/format/github"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||||
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/format/table"
|
||||||
|
"github.com/anchore/syft/syft/format/template"
|
||||||
|
"github.com/anchore/syft/syft/format/text"
|
||||||
"github.com/anchore/syft/syft/sbom"
|
"github.com/anchore/syft/syft/sbom"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FormatAliases(ids ...sbom.FormatID) (aliases []string) {
|
var _ clio.PostLoader = (*Format)(nil)
|
||||||
for _, id := range ids {
|
|
||||||
switch id {
|
// Format contains all user configuration for output formatting.
|
||||||
case syftjson.ID:
|
type Format struct {
|
||||||
aliases = append(aliases, "syft-json")
|
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||||
case text.ID:
|
Template FormatTemplate `yaml:"template" json:"template" mapstructure:"template"`
|
||||||
aliases = append(aliases, "text")
|
SyftJSON FormatSyftJSON `yaml:"json" json:"json" mapstructure:"json"`
|
||||||
case table.ID:
|
SPDXJSON FormatSPDXJSON `yaml:"spdx-json" json:"spdx-json" mapstructure:"spdx-json"`
|
||||||
aliases = append(aliases, "table")
|
CyclonedxJSON FormatCyclonedxJSON `yaml:"cyclonedx-json" json:"cyclonedx-json" mapstructure:"cyclonedx-json"`
|
||||||
case spdxjson.ID:
|
CyclonedxXML FormatCyclonedxXML `yaml:"cyclonedx-xml" json:"cyclonedx-xml" mapstructure:"cyclonedx-xml"`
|
||||||
aliases = append(aliases, "spdx-json")
|
}
|
||||||
case spdxtagvalue.ID:
|
|
||||||
aliases = append(aliases, "spdx-tag-value")
|
func (o *Format) PostLoad() error {
|
||||||
case cyclonedxxml.ID:
|
o.SyftJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SyftJSON.Pretty)
|
||||||
aliases = append(aliases, "cyclonedx-xml")
|
o.SPDXJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.SPDXJSON.Pretty)
|
||||||
case cyclonedxjson.ID:
|
o.CyclonedxJSON.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxJSON.Pretty)
|
||||||
aliases = append(aliases, "cyclonedx-json")
|
o.CyclonedxXML.Pretty = multiLevelOption[bool](false, o.Pretty, o.CyclonedxXML.Pretty)
|
||||||
case github.ID:
|
|
||||||
aliases = append(aliases, "github", "github-json")
|
return nil
|
||||||
default:
|
}
|
||||||
aliases = append(aliases, string(id))
|
|
||||||
|
func DefaultFormat() Format {
|
||||||
|
return Format{
|
||||||
|
Template: DefaultFormatTemplate(),
|
||||||
|
SyftJSON: DefaultFormatJSON(),
|
||||||
|
SPDXJSON: DefaultFormatSPDXJSON(),
|
||||||
|
CyclonedxJSON: DefaultFormatCyclonedxJSON(),
|
||||||
|
CyclonedxXML: DefaultFormatCyclonedxXML(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Format) Encoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
// setup all encoders based on the configuration
|
||||||
|
var list encoderList
|
||||||
|
|
||||||
|
// in the future there will be application configuration options that can be used to set the default output format
|
||||||
|
list.addWithErr(template.ID)(o.Template.formatEncoders())
|
||||||
|
list.addWithErr(syftjson.ID)(o.SyftJSON.formatEncoders())
|
||||||
|
list.add(table.ID)(table.NewFormatEncoder())
|
||||||
|
list.add(text.ID)(text.NewFormatEncoder())
|
||||||
|
list.add(github.ID)(github.NewFormatEncoder())
|
||||||
|
list.addWithErr(cyclonedxxml.ID)(o.CyclonedxXML.formatEncoders())
|
||||||
|
list.addWithErr(cyclonedxjson.ID)(o.CyclonedxJSON.formatEncoders())
|
||||||
|
list.addWithErr(spdxjson.ID)(o.SPDXJSON.formatEncoders())
|
||||||
|
list.addWithErr(spdxtagvalue.ID)(spdxTagValueEncoders())
|
||||||
|
|
||||||
|
return list.encoders, list.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when application configuration is made for this format then this should be ported to the options object
|
||||||
|
// that is created for that configuration (as done with the template output option)
|
||||||
|
func spdxTagValueEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
var (
|
||||||
|
encs []sbom.FormatEncoder
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
for _, v := range spdxtagvalue.SupportedVersions() {
|
||||||
|
enc, err := spdxtagvalue.NewFormatEncoderWithConfig(spdxtagvalue.EncoderConfig{Version: v})
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
} else {
|
||||||
|
encs = append(encs, enc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return aliases
|
return encs, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
type encoderList struct {
|
||||||
|
encoders []sbom.FormatEncoder
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *encoderList) addWithErr(name sbom.FormatID) func([]sbom.FormatEncoder, error) {
|
||||||
|
return func(encs []sbom.FormatEncoder, err error) {
|
||||||
|
if err != nil {
|
||||||
|
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: %w", name, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, enc := range encs {
|
||||||
|
if enc == nil {
|
||||||
|
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: nil encoder returned", name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.encoders = append(l.encoders, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *encoderList) add(name sbom.FormatID) func(...sbom.FormatEncoder) {
|
||||||
|
return func(encs ...sbom.FormatEncoder) {
|
||||||
|
for _, enc := range encs {
|
||||||
|
if enc == nil {
|
||||||
|
l.err = multierror.Append(l.err, fmt.Errorf("unable to configure %q format encoder: nil encoder returned", name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.encoders = append(l.encoders, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func multiLevelOption[T any](defaultValue T, option ...*T) *T {
|
||||||
|
result := defaultValue
|
||||||
|
for _, opt := range option {
|
||||||
|
if opt != nil {
|
||||||
|
result = *opt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FormatCyclonedxJSON struct {
|
||||||
|
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultFormatCyclonedxJSON() FormatCyclonedxJSON {
|
||||||
|
return FormatCyclonedxJSON{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o FormatCyclonedxJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
var (
|
||||||
|
encs []sbom.FormatEncoder
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
for _, v := range cyclonedxjson.SupportedVersions() {
|
||||||
|
enc, err := cyclonedxjson.NewFormatEncoderWithConfig(o.buildConfig(v))
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
} else {
|
||||||
|
encs = append(encs, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encs, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o FormatCyclonedxJSON) buildConfig(version string) cyclonedxjson.EncoderConfig {
|
||||||
|
var pretty bool
|
||||||
|
if o.Pretty != nil {
|
||||||
|
pretty = *o.Pretty
|
||||||
|
}
|
||||||
|
return cyclonedxjson.EncoderConfig{
|
||||||
|
Version: version,
|
||||||
|
Pretty: pretty,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatCyclonedxJSON_buildConfig(t *testing.T) {
|
||||||
|
// assert when building the config that we respond to all possible fields
|
||||||
|
|
||||||
|
ft := &FormatCyclonedxJSON{}
|
||||||
|
ft = setAllToNonZero(t, ft).(*FormatCyclonedxJSON)
|
||||||
|
|
||||||
|
subject := ft.buildConfig("Version")
|
||||||
|
assertExpectedValue(t, subject)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setAllToNonZero(t testing.TB, structPtr any) any {
|
||||||
|
// set all fields on the struct to non-zero values
|
||||||
|
rt := reflect.TypeOf(structPtr)
|
||||||
|
if rt.Kind() != reflect.Ptr || rt.Elem().Kind() != reflect.Struct {
|
||||||
|
t.Fatal("expected a pointer to a struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := reflect.ValueOf(structPtr).Elem()
|
||||||
|
for i := 0; i < rt.Elem().NumField(); i++ {
|
||||||
|
val := getNonZeroExampleValue(t, rv.Field(i).Interface(), rt.Elem().Field(i).Name)
|
||||||
|
rv.Field(i).Set(reflect.ValueOf(val))
|
||||||
|
}
|
||||||
|
return structPtr
|
||||||
|
}
|
||||||
|
|
||||||
|
func getNonZeroExampleValue(t testing.TB, v any, name string) any {
|
||||||
|
switch v.(type) {
|
||||||
|
case bool:
|
||||||
|
return true
|
||||||
|
case *bool:
|
||||||
|
val := true
|
||||||
|
return &val
|
||||||
|
case string:
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
t.Fatalf("unsupported type: %T", v)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertExpectedValue(t *testing.T, structTy any) {
|
||||||
|
rt := reflect.TypeOf(structTy)
|
||||||
|
rv := reflect.ValueOf(structTy)
|
||||||
|
|
||||||
|
for i := 0; i < rt.NumField(); i++ {
|
||||||
|
f := rt.Field(i)
|
||||||
|
fieldValue := rv.Field(i)
|
||||||
|
|
||||||
|
if fieldValue.Type().Kind() == reflect.String {
|
||||||
|
// use the field name as the expected value
|
||||||
|
assert.Equalf(t, f.Name, fieldValue.String(), "field %q value differs", f.Name)
|
||||||
|
} else {
|
||||||
|
// use the zero value for the type
|
||||||
|
if reflect.DeepEqual(fieldValue.Interface(), reflect.Zero(fieldValue.Type()).Interface()) {
|
||||||
|
t.Errorf("field '%s' is zero", f.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FormatCyclonedxXML struct {
|
||||||
|
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultFormatCyclonedxXML() FormatCyclonedxXML {
|
||||||
|
return FormatCyclonedxXML{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o FormatCyclonedxXML) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
var (
|
||||||
|
encs []sbom.FormatEncoder
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
for _, v := range cyclonedxxml.SupportedVersions() {
|
||||||
|
enc, err := cyclonedxxml.NewFormatEncoderWithConfig(o.buildConfig(v))
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
} else {
|
||||||
|
encs = append(encs, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encs, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o FormatCyclonedxXML) buildConfig(version string) cyclonedxxml.EncoderConfig {
|
||||||
|
var pretty bool
|
||||||
|
if o.Pretty != nil {
|
||||||
|
pretty = *o.Pretty
|
||||||
|
}
|
||||||
|
return cyclonedxxml.EncoderConfig{
|
||||||
|
Version: version,
|
||||||
|
Pretty: pretty,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatCyclonedxXML_buildConfig(t *testing.T) {
|
||||||
|
// assert when building the config that we respond to all possible fields
|
||||||
|
|
||||||
|
ft := FormatCyclonedxXML{}
|
||||||
|
ftp := setAllToNonZero(t, &ft).(*FormatCyclonedxXML)
|
||||||
|
|
||||||
|
subject := ftp.buildConfig("Version")
|
||||||
|
assertExpectedValue(t, subject)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format/spdxjson"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FormatSPDXJSON struct {
|
||||||
|
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultFormatSPDXJSON() FormatSPDXJSON {
|
||||||
|
return FormatSPDXJSON{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o FormatSPDXJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
var (
|
||||||
|
encs []sbom.FormatEncoder
|
||||||
|
errs error
|
||||||
|
)
|
||||||
|
for _, v := range spdxjson.SupportedVersions() {
|
||||||
|
enc, err := spdxjson.NewFormatEncoderWithConfig(o.buildConfig(v))
|
||||||
|
if err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
} else {
|
||||||
|
encs = append(encs, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return encs, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o FormatSPDXJSON) buildConfig(v string) spdxjson.EncoderConfig {
|
||||||
|
var pretty bool
|
||||||
|
if o.Pretty != nil {
|
||||||
|
pretty = *o.Pretty
|
||||||
|
}
|
||||||
|
return spdxjson.EncoderConfig{
|
||||||
|
Version: v,
|
||||||
|
Pretty: pretty,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatSPDXJSON_buildConfig(t *testing.T) {
|
||||||
|
// assert when building the config that we respond to all possible fields
|
||||||
|
|
||||||
|
ft := &FormatSPDXJSON{}
|
||||||
|
ft = setAllToNonZero(t, ft).(*FormatSPDXJSON)
|
||||||
|
|
||||||
|
subject := ft.buildConfig("Version")
|
||||||
|
assertExpectedValue(t, subject)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FormatSyftJSON struct {
|
||||||
|
Legacy bool `yaml:"legacy" json:"legacy" mapstructure:"legacy"`
|
||||||
|
Pretty *bool `yaml:"pretty" json:"pretty" mapstructure:"pretty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultFormatJSON() FormatSyftJSON {
|
||||||
|
return FormatSyftJSON{
|
||||||
|
Legacy: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o FormatSyftJSON) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
enc, err := syftjson.NewFormatEncoderWithConfig(o.buildConfig())
|
||||||
|
return []sbom.FormatEncoder{enc}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o FormatSyftJSON) buildConfig() syftjson.EncoderConfig {
|
||||||
|
var pretty bool
|
||||||
|
if o.Pretty != nil {
|
||||||
|
pretty = *o.Pretty
|
||||||
|
}
|
||||||
|
return syftjson.EncoderConfig{
|
||||||
|
Legacy: o.Legacy,
|
||||||
|
Pretty: pretty,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFormatSyftJSON_buildConfig(t *testing.T) {
|
||||||
|
// assert when building the config that we respond to all possible fields
|
||||||
|
|
||||||
|
ft := &FormatSyftJSON{}
|
||||||
|
ft = setAllToNonZero(t, ft).(*FormatSyftJSON)
|
||||||
|
|
||||||
|
subject := ft.buildConfig()
|
||||||
|
assertExpectedValue(t, subject)
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/syft/format/template"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ clio.FlagAdder = (*FormatTemplate)(nil)
|
||||||
|
|
||||||
|
type FormatTemplate struct {
|
||||||
|
Enabled bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||||
|
Path string `yaml:"path" json:"path" mapstructure:"path"` // -t template file to use for output
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultFormatTemplate() FormatTemplate {
|
||||||
|
return FormatTemplate{
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *FormatTemplate) AddFlags(flags clio.FlagSet) {
|
||||||
|
if o.Enabled {
|
||||||
|
flags.StringVarP(&o.Path, "template", "t",
|
||||||
|
"specify the path to a Go template file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o FormatTemplate) formatEncoders() ([]sbom.FormatEncoder, error) {
|
||||||
|
if !o.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
enc, err := template.NewFormatEncoder(template.EncoderConfig{
|
||||||
|
TemplatePath: o.Path,
|
||||||
|
})
|
||||||
|
return []sbom.FormatEncoder{enc}, err
|
||||||
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const defaultFulcioURL = "https://fulcio.sigstore.dev"
|
|
||||||
|
|
||||||
// FulcioOptions is the wrapper for Fulcio related options.
|
|
||||||
type FulcioOptions struct {
|
|
||||||
URL string
|
|
||||||
IdentityToken string
|
|
||||||
InsecureSkipFulcioVerify bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Interface = (*FulcioOptions)(nil)
|
|
||||||
|
|
||||||
// AddFlags implements Interface
|
|
||||||
func (o *FulcioOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
|
||||||
// TODO: change this back to api.SigstorePublicServerURL after the v1 migration is complete.
|
|
||||||
cmd.Flags().StringVar(&o.URL, "fulcio-url", defaultFulcioURL,
|
|
||||||
"address of sigstore PKI server")
|
|
||||||
|
|
||||||
cmd.Flags().StringVar(&o.IdentityToken, "identity-token", "",
|
|
||||||
"identity token to use for certificate from fulcio")
|
|
||||||
|
|
||||||
cmd.Flags().BoolVar(&o.InsecureSkipFulcioVerify, "insecure-skip-verify", false,
|
|
||||||
"skip verifying fulcio certificat and the SCT (Signed Certificate Timestamp) (this should only be used for testing).")
|
|
||||||
return bindFulcioConfigOptions(cmd.Flags(), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindFulcioConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
|
||||||
if err := v.BindPFlag("attest.fulcio-url", flags.Lookup("fulcio-url")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("attest.fulcio-identity-token", flags.Lookup("identity-token")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("attest.insecure-skip-verify", flags.Lookup("insecure-skip-verify")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
type golangConfig struct {
|
||||||
|
SearchLocalModCacheLicenses bool `json:"search-local-mod-cache-licenses" yaml:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"`
|
||||||
|
LocalModCacheDir string `json:"local-mod-cache-dir" yaml:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"`
|
||||||
|
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||||
|
Proxy string `json:"proxy" yaml:"proxy" mapstructure:"proxy"`
|
||||||
|
NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
type javaConfig struct {
|
||||||
|
UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"`
|
||||||
|
MavenURL string `yaml:"maven-url" json:"maven-url" mapstructure:"maven-url"`
|
||||||
|
MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
type javaScriptConfig struct {
|
||||||
|
SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"`
|
||||||
|
NpmBaseURL string `json:"npm-base-url" yaml:"npm-base-url" mapstructure:"npm-base-url"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
type linuxKernelConfig struct {
|
||||||
|
CatalogModules bool `json:"catalog-modules" yaml:"catalog-modules" mapstructure:"catalog-modules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultLinuxKernelConfig() linuxKernelConfig {
|
||||||
|
return linuxKernelConfig{
|
||||||
|
CatalogModules: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const DefaultOIDCIssuerURL = "https://oauth2.sigstore.dev/auth"
|
|
||||||
|
|
||||||
// OIDCOptions is the wrapper for OIDC related options.
|
|
||||||
type OIDCOptions struct {
|
|
||||||
Issuer string
|
|
||||||
ClientID string
|
|
||||||
RedirectURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Interface = (*OIDCOptions)(nil)
|
|
||||||
|
|
||||||
// AddFlags implements Interface
|
|
||||||
func (o *OIDCOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
|
||||||
cmd.Flags().StringVar(&o.Issuer, "oidc-issuer", DefaultOIDCIssuerURL,
|
|
||||||
"OIDC provider to be used to issue ID token")
|
|
||||||
|
|
||||||
cmd.Flags().StringVar(&o.ClientID, "oidc-client-id", "sigstore",
|
|
||||||
"OIDC client ID for application")
|
|
||||||
|
|
||||||
cmd.Flags().StringVar(&o.RedirectURL, "oidc-redirect-url", "",
|
|
||||||
"OIDC redirect URL (Optional)")
|
|
||||||
|
|
||||||
return bindOIDCConfigOptions(cmd.Flags(), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindOIDCConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
|
||||||
if err := v.BindPFlag("attest.oidc-issuer", flags.Lookup("oidc-issuer")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("attest.oidc-client-id", flags.Lookup("oidc-client-id")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("attest.oidc-redirect-url", flags.Lookup("oidc-redirect-url")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Interface interface {
|
|
||||||
// AddFlags adds this options' flags to the cobra command.
|
|
||||||
AddFlags(cmd *cobra.Command, v *viper.Viper) error
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||||
|
"github.com/anchore/syft/syft/format/github"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||||
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/format/table"
|
||||||
|
"github.com/anchore/syft/syft/format/template"
|
||||||
|
"github.com/anchore/syft/syft/format/text"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
clio.FlagAdder
|
||||||
|
clio.PostLoader
|
||||||
|
} = (*Output)(nil)
|
||||||
|
|
||||||
|
// Output has the standard output options syft accepts: multiple -o, --file, --template
|
||||||
|
type Output struct {
|
||||||
|
AllowableOptions []string `yaml:"-" json:"-" mapstructure:"-"`
|
||||||
|
AllowMultipleOutputs bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||||
|
AllowToFile bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||||
|
Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output
|
||||||
|
OutputFile `yaml:",inline" json:"" mapstructure:",squash"`
|
||||||
|
Format `yaml:"format" json:"format" mapstructure:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultOutput() Output {
|
||||||
|
return Output{
|
||||||
|
AllowMultipleOutputs: true,
|
||||||
|
AllowToFile: true,
|
||||||
|
Outputs: []string{string(table.ID)},
|
||||||
|
OutputFile: OutputFile{
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
Format: DefaultFormat(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Output) PostLoad() error {
|
||||||
|
var errs error
|
||||||
|
for _, loader := range []clio.PostLoader{&o.OutputFile, &o.Format} {
|
||||||
|
if err := loader.PostLoad(); err != nil {
|
||||||
|
errs = multierror.Append(errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Output) AddFlags(flags clio.FlagSet) {
|
||||||
|
var names []string
|
||||||
|
for _, id := range supportedIDs() {
|
||||||
|
names = append(names, id.String())
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
flags.StringArrayVarP(&o.Outputs, "output", "o",
|
||||||
|
fmt.Sprintf("report output format (<format>=<file> to output to a file), formats=%v", names))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Output) SBOMWriter() (sbom.Writer, error) {
|
||||||
|
names := o.OutputNameSet()
|
||||||
|
|
||||||
|
if len(o.Outputs) > 1 && !o.AllowMultipleOutputs {
|
||||||
|
return nil, fmt.Errorf("only one output format is allowed (given %d: %s)", len(o.Outputs), names)
|
||||||
|
}
|
||||||
|
|
||||||
|
usesTemplateOutput := names.Has(string(template.ID))
|
||||||
|
|
||||||
|
if usesTemplateOutput && o.Format.Template.Path == "" {
|
||||||
|
return nil, fmt.Errorf(`must specify path to template file when using "template" output format`)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoders, err := o.Encoders()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !o.AllowToFile {
|
||||||
|
for _, opt := range o.Outputs {
|
||||||
|
if strings.Contains(opt, "=") {
|
||||||
|
return nil, fmt.Errorf("file output is not allowed ('-o format=path' should be '-o format')")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeSBOMWriter(o.Outputs, o.LegacyFile, encoders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o Output) OutputNameSet() *strset.Set {
|
||||||
|
names := strset.New()
|
||||||
|
for _, output := range o.Outputs {
|
||||||
|
fields := strings.Split(output, "=")
|
||||||
|
names.Add(fields[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func supportedIDs() []sbom.FormatID {
|
||||||
|
encs := []sbom.FormatID{
|
||||||
|
// encoders that support a single version
|
||||||
|
syftjson.ID,
|
||||||
|
github.ID,
|
||||||
|
table.ID,
|
||||||
|
text.ID,
|
||||||
|
template.ID,
|
||||||
|
|
||||||
|
// encoders that support multiple versions
|
||||||
|
cyclonedxxml.ID,
|
||||||
|
cyclonedxjson.ID,
|
||||||
|
spdxtagvalue.ID,
|
||||||
|
spdxjson.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
return encs
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mitchellh/go-homedir"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/fangs"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
clio.FlagAdder
|
||||||
|
clio.PostLoader
|
||||||
|
} = (*OutputFile)(nil)
|
||||||
|
|
||||||
|
// Deprecated: OutputFile supports the --file to write the SBOM output to
|
||||||
|
type OutputFile struct {
|
||||||
|
Enabled bool `yaml:"-" json:"-" mapstructure:"-"`
|
||||||
|
LegacyFile string `yaml:"-" json:"-" mapstructure:"legacyFile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OutputFile) AddFlags(flags clio.FlagSet) {
|
||||||
|
if o.Enabled {
|
||||||
|
flags.StringVarP(&o.LegacyFile, "file", "",
|
||||||
|
"file to write the default report output to (default is STDOUT)")
|
||||||
|
|
||||||
|
if pfp, ok := flags.(fangs.PFlagSetProvider); ok {
|
||||||
|
flagSet := pfp.PFlagSet()
|
||||||
|
flagSet.Lookup("file").Deprecated = "use: output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OutputFile) PostLoad() error {
|
||||||
|
if !o.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if o.LegacyFile != "" {
|
||||||
|
file, err := expandFilePath(o.LegacyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
o.LegacyFile = file
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OutputFile) SBOMWriter(f sbom.FormatEncoder) (sbom.Writer, error) {
|
||||||
|
if !o.Enabled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
writer, err := newSBOMMultiWriter(newSBOMWriterDescription(f, o.LegacyFile))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandFilePath(file string) (string, error) {
|
||||||
|
if file != "" {
|
||||||
|
expandedPath, err := homedir.Expand(file)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to expand file path=%q: %w", file, err)
|
||||||
|
}
|
||||||
|
file = expandedPath
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/cyclonedxxml"
|
||||||
|
"github.com/anchore/syft/syft/format/github"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxjson"
|
||||||
|
"github.com/anchore/syft/syft/format/spdxtagvalue"
|
||||||
|
"github.com/anchore/syft/syft/format/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/format/table"
|
||||||
|
"github.com/anchore/syft/syft/format/template"
|
||||||
|
"github.com/anchore/syft/syft/format/text"
|
||||||
|
"github.com/anchore/syft/syft/sbom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_getEncoders(t *testing.T) {
|
||||||
|
allDefaultEncoderNames := strset.New()
|
||||||
|
for _, id := range supportedIDs() {
|
||||||
|
allDefaultEncoderNames.Add(id.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := DefaultOutput()
|
||||||
|
require.NoError(t, opts.Format.PostLoad())
|
||||||
|
opts.Format.Template.Path = "somewhere"
|
||||||
|
|
||||||
|
encoders, err := opts.Encoders()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, encoders)
|
||||||
|
|
||||||
|
encoderNames := strset.New()
|
||||||
|
for _, e := range encoders {
|
||||||
|
encoderNames.Add(e.ID().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, allDefaultEncoderNames.List(), encoderNames.List(), "not all encoders are expressed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_EncoderCollection_ByString_IDOnly_Defaults(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want sbom.FormatID
|
||||||
|
}{
|
||||||
|
// SPDX Tag-Value
|
||||||
|
{
|
||||||
|
name: "spdx",
|
||||||
|
want: spdxtagvalue.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdx-tag-value",
|
||||||
|
want: spdxtagvalue.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdx-tv",
|
||||||
|
want: spdxtagvalue.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdxtv", // clean variant
|
||||||
|
want: spdxtagvalue.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// SPDX JSON
|
||||||
|
{
|
||||||
|
name: "spdx-json",
|
||||||
|
want: spdxjson.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "spdxjson", // clean variant
|
||||||
|
want: spdxjson.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cyclonedx JSON
|
||||||
|
{
|
||||||
|
name: "cyclonedx-json",
|
||||||
|
want: cyclonedxjson.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclonedxjson", // clean variant
|
||||||
|
want: cyclonedxjson.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cyclonedx XML
|
||||||
|
{
|
||||||
|
name: "cdx",
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclone",
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclonedx",
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclonedx-xml",
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cyclonedxxml", // clean variant
|
||||||
|
want: cyclonedxxml.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Syft Table
|
||||||
|
{
|
||||||
|
name: "table",
|
||||||
|
want: table.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "syft-table",
|
||||||
|
want: table.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Syft Text
|
||||||
|
{
|
||||||
|
name: "text",
|
||||||
|
want: text.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "syft-text",
|
||||||
|
want: text.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Syft JSON
|
||||||
|
{
|
||||||
|
name: "json",
|
||||||
|
want: syftjson.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "syft-json",
|
||||||
|
want: syftjson.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "syftjson", // clean variant
|
||||||
|
want: syftjson.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// GitHub JSON
|
||||||
|
{
|
||||||
|
name: "github",
|
||||||
|
want: github.ID,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "github-json",
|
||||||
|
want: github.ID,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Syft template
|
||||||
|
{
|
||||||
|
name: "template",
|
||||||
|
want: template.ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := DefaultOutput()
|
||||||
|
require.NoError(t, opts.Format.PostLoad())
|
||||||
|
opts.Format.Template.Path = "somewhere"
|
||||||
|
|
||||||
|
defaultEncoders, err := opts.Encoders()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
encoders := format.NewEncoderCollection(defaultEncoders...)
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
f := encoders.GetByString(tt.name)
|
||||||
|
if tt.want == "" {
|
||||||
|
require.Nil(t, f)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NotNil(t, f)
|
||||||
|
assert.Equal(t, tt.want, f.ID())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_OutputHonorsAllowFile(t *testing.T) {
|
||||||
|
o := DefaultOutput()
|
||||||
|
|
||||||
|
t.Run("file is not allowed", func(t *testing.T) {
|
||||||
|
o.AllowToFile = false
|
||||||
|
o.Outputs = []string{"table=/tmp/somefile"}
|
||||||
|
|
||||||
|
w, err := o.SBOMWriter()
|
||||||
|
assert.Nil(t, w)
|
||||||
|
assert.ErrorContains(t, err, "file output is not allowed")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("file is allowed", func(t *testing.T) {
|
||||||
|
o.AllowToFile = true
|
||||||
|
o.Outputs = []string{"table=/tmp/somefile"}
|
||||||
|
|
||||||
|
w, err := o.SBOMWriter()
|
||||||
|
assert.NotNil(t, w)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft"
|
|
||||||
"github.com/anchore/syft/syft/formats/table"
|
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
type PackagesOptions struct {
|
|
||||||
Scope string
|
|
||||||
Output []string
|
|
||||||
OutputTemplatePath string
|
|
||||||
File string
|
|
||||||
Platform string
|
|
||||||
Exclude []string
|
|
||||||
Catalogers []string
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Interface = (*PackagesOptions)(nil)
|
|
||||||
|
|
||||||
func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
|
||||||
cmd.Flags().StringVarP(&o.Scope, "scope", "s", cataloger.DefaultSearchConfig().Scope.String(),
|
|
||||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
|
||||||
|
|
||||||
cmd.Flags().StringArrayVarP(&o.Output, "output", "o", FormatAliases(table.ID),
|
|
||||||
fmt.Sprintf("report output format, options=%v", FormatAliases(syft.FormatIDs()...)))
|
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&o.File, "file", "", "",
|
|
||||||
"file to write the default report output to (default is STDOUT)")
|
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&o.OutputTemplatePath, "template", "t", "",
|
|
||||||
"specify the path to a Go template file")
|
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&o.Platform, "platform", "", "",
|
|
||||||
"an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')")
|
|
||||||
|
|
||||||
cmd.Flags().StringArrayVarP(&o.Exclude, "exclude", "", nil,
|
|
||||||
"exclude paths from being scanned using a glob expression")
|
|
||||||
|
|
||||||
cmd.Flags().StringArrayVarP(&o.Catalogers, "catalogers", "", nil,
|
|
||||||
"enable one or more package catalogers")
|
|
||||||
|
|
||||||
cmd.Flags().StringVarP(&o.Name, "name", "", "",
|
|
||||||
"set the name of the target being analyzed")
|
|
||||||
|
|
||||||
return bindPackageConfigOptions(cmd.Flags(), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
|
||||||
// Formatting & Input options //////////////////////////////////////////////
|
|
||||||
|
|
||||||
if err := v.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("file", flags.Lookup("file")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("exclude", flags.Lookup("exclude")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("catalogers", flags.Lookup("catalogers")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("name", flags.Lookup("name")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("output", flags.Lookup("output")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("output-template-path", flags.Lookup("template")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := v.BindPFlag("platform", flags.Lookup("platform")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import "github.com/anchore/syft/syft/cataloging"
|
||||||
|
|
||||||
|
type packageConfig struct {
|
||||||
|
SearchUnindexedArchives bool `yaml:"search-unindexed-archives" json:"search-unindexed-archives" mapstructure:"search-unindexed-archives"`
|
||||||
|
SearchIndexedArchives bool `yaml:"search-indexed-archives" json:"search-indexed-archives" mapstructure:"search-indexed-archives"`
|
||||||
|
ExcludeBinaryOverlapByOwnership bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"` // exclude synthetic binary packages owned by os package files
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultPackageConfig() packageConfig {
|
||||||
|
c := cataloging.DefaultArchiveSearchConfig()
|
||||||
|
return packageConfig{
|
||||||
|
SearchIndexedArchives: c.IncludeIndexedArchives,
|
||||||
|
SearchUnindexedArchives: c.IncludeUnindexedArchives,
|
||||||
|
ExcludeBinaryOverlapByOwnership: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
type pythonConfig struct {
|
||||||
|
GuessUnpinnedRequirements bool `json:"guess-unpinned-requirements" yaml:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/stereoscope/pkg/image"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RegistryCredentials struct {
|
||||||
|
Authority string `yaml:"authority" json:"authority" mapstructure:"authority"`
|
||||||
|
// IMPORTANT: do not show any credential information, use secret type to automatically redact the values
|
||||||
|
Username secret `yaml:"username" json:"username" mapstructure:"username"`
|
||||||
|
Password secret `yaml:"password" json:"password" mapstructure:"password"`
|
||||||
|
Token secret `yaml:"token" json:"token" mapstructure:"token"`
|
||||||
|
|
||||||
|
TLSCert string `yaml:"tls-cert,omitempty" json:"tls-cert,omitempty" mapstructure:"tls-cert"`
|
||||||
|
TLSKey string `yaml:"tls-key,omitempty" json:"tls-key,omitempty" mapstructure:"tls-key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type registryConfig struct {
|
||||||
|
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
|
||||||
|
InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
|
||||||
|
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
|
||||||
|
CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ clio.PostLoader = (*registryConfig)(nil)
|
||||||
|
|
||||||
|
func (cfg *registryConfig) PostLoad() error {
|
||||||
|
// there may be additional credentials provided by env var that should be appended to the set of credentials
|
||||||
|
authority, username, password, token, tlsCert, tlsKey :=
|
||||||
|
os.Getenv("SYFT_REGISTRY_AUTH_AUTHORITY"),
|
||||||
|
os.Getenv("SYFT_REGISTRY_AUTH_USERNAME"),
|
||||||
|
os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"),
|
||||||
|
os.Getenv("SYFT_REGISTRY_AUTH_TOKEN"),
|
||||||
|
os.Getenv("SYFT_REGISTRY_AUTH_TLS_CERT"),
|
||||||
|
os.Getenv("SYFT_REGISTRY_AUTH_TLS_KEY")
|
||||||
|
|
||||||
|
if hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey) {
|
||||||
|
// note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.
|
||||||
|
// since this PostLoad is called before the PostLoad on the Auth credentials list,
|
||||||
|
// all appropriate redactions will be added
|
||||||
|
cfg.Auth = append([]RegistryCredentials{
|
||||||
|
{
|
||||||
|
Authority: authority,
|
||||||
|
Username: secret(username),
|
||||||
|
Password: secret(password),
|
||||||
|
Token: secret(token),
|
||||||
|
TLSCert: tlsCert,
|
||||||
|
TLSKey: tlsKey,
|
||||||
|
},
|
||||||
|
}, cfg.Auth...)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey string) bool {
|
||||||
|
hasUserPass := username != "" && password != ""
|
||||||
|
hasToken := token != ""
|
||||||
|
hasTLSMaterial := tlsCert != "" && tlsKey != ""
|
||||||
|
return hasUserPass || hasToken || hasTLSMaterial
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *registryConfig) ToOptions() *image.RegistryOptions {
|
||||||
|
var auth = make([]image.RegistryCredentials, len(cfg.Auth))
|
||||||
|
for i, a := range cfg.Auth {
|
||||||
|
auth[i] = image.RegistryCredentials{
|
||||||
|
Authority: a.Authority,
|
||||||
|
Username: a.Username.String(),
|
||||||
|
Password: a.Password.String(),
|
||||||
|
Token: a.Token.String(),
|
||||||
|
ClientCert: a.TLSCert,
|
||||||
|
ClientKey: a.TLSKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &image.RegistryOptions{
|
||||||
|
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
|
||||||
|
InsecureUseHTTP: cfg.InsecureUseHTTP,
|
||||||
|
Credentials: auth,
|
||||||
|
CAFileOrDir: cfg.CACert,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package config
|
package options
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -11,48 +11,60 @@ import (
|
|||||||
|
|
||||||
func TestHasNonEmptyCredentials(t *testing.T) {
|
func TestHasNonEmptyCredentials(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
username, password, token string
|
username, password, token, cert, key string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
|
|
||||||
{
|
{
|
||||||
"", "", "",
|
"", "", "", "", "",
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"user", "", "",
|
"user", "", "", "", "",
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"", "pass", "",
|
"", "pass", "", "", "",
|
||||||
false,
|
false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"", "pass", "tok",
|
"", "pass", "tok", "", "",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"user", "", "tok",
|
"user", "", "tok", "", "",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"", "", "tok",
|
"", "", "tok", "", "",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"user", "pass", "tok",
|
"user", "pass", "tok", "", "",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"user", "pass", "",
|
"user", "pass", "", "", "",
|
||||||
true,
|
true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"", "", "", "cert", "key",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"", "", "", "cert", "",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"", "", "", "", "key",
|
||||||
|
false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
|
||||||
assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token))
|
assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token, test.cert, test.key))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,19 +72,19 @@ func TestHasNonEmptyCredentials(t *testing.T) {
|
|||||||
func Test_registry_ToOptions(t *testing.T) {
|
func Test_registry_ToOptions(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input registry
|
input registryConfig
|
||||||
expected image.RegistryOptions
|
expected image.RegistryOptions
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no registry options",
|
name: "no registry options",
|
||||||
input: registry{},
|
input: registryConfig{},
|
||||||
expected: image.RegistryOptions{
|
expected: image.RegistryOptions{
|
||||||
Credentials: []image.RegistryCredentials{},
|
Credentials: []image.RegistryCredentials{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "set InsecureSkipTLSVerify",
|
name: "set InsecureSkipTLSVerify",
|
||||||
input: registry{
|
input: registryConfig{
|
||||||
InsecureSkipTLSVerify: true,
|
InsecureSkipTLSVerify: true,
|
||||||
},
|
},
|
||||||
expected: image.RegistryOptions{
|
expected: image.RegistryOptions{
|
||||||
@@ -82,7 +94,7 @@ func Test_registry_ToOptions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "set InsecureUseHTTP",
|
name: "set InsecureUseHTTP",
|
||||||
input: registry{
|
input: registryConfig{
|
||||||
InsecureUseHTTP: true,
|
InsecureUseHTTP: true,
|
||||||
},
|
},
|
||||||
expected: image.RegistryOptions{
|
expected: image.RegistryOptions{
|
||||||
@@ -92,7 +104,7 @@ func Test_registry_ToOptions(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "set all bool options",
|
name: "set all bool options",
|
||||||
input: registry{
|
input: registryConfig{
|
||||||
InsecureSkipTLSVerify: true,
|
InsecureSkipTLSVerify: true,
|
||||||
InsecureUseHTTP: true,
|
InsecureUseHTTP: true,
|
||||||
},
|
},
|
||||||
@@ -102,6 +114,29 @@ func Test_registry_ToOptions(t *testing.T) {
|
|||||||
Credentials: []image.RegistryCredentials{},
|
Credentials: []image.RegistryCredentials{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "provide all tls configuration",
|
||||||
|
input: registryConfig{
|
||||||
|
CACert: "ca.crt",
|
||||||
|
InsecureSkipTLSVerify: true,
|
||||||
|
Auth: []RegistryCredentials{
|
||||||
|
{
|
||||||
|
TLSCert: "client.crt",
|
||||||
|
TLSKey: "client.key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: image.RegistryOptions{
|
||||||
|
CAFileOrDir: "ca.crt",
|
||||||
|
InsecureSkipTLSVerify: true,
|
||||||
|
Credentials: []image.RegistryCredentials{
|
||||||
|
{
|
||||||
|
ClientCert: "client.crt",
|
||||||
|
ClientKey: "client.key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const DefaultRekorURL = "https://rekor.sigstore.dev"
|
|
||||||
|
|
||||||
// RekorOptions is the wrapper for Rekor related options.
|
|
||||||
type RekorOptions struct {
|
|
||||||
URL string
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Interface = (*RekorOptions)(nil)
|
|
||||||
|
|
||||||
// AddFlags implements Interface
|
|
||||||
func (o *RekorOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
|
||||||
cmd.Flags().StringVar(&o.URL, "rekor-url", DefaultRekorURL,
|
|
||||||
"address of rekor STL server")
|
|
||||||
return bindRekorConfigOptions(cmd.Flags(), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindRekorConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
|
||||||
// TODO: config re-design
|
|
||||||
if err := v.BindPFlag("attest.rekor-url", flags.Lookup("rekor-url")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import "github.com/anchore/fangs"
|
||||||
|
|
||||||
|
var _ fangs.FieldDescriber = (*relationshipsConfig)(nil)
|
||||||
|
|
||||||
|
type relationshipsConfig struct {
|
||||||
|
PackageFileOwnership bool `mapstructure:"package-file-ownership" json:"package-file-ownership" yaml:"package-file-ownership"`
|
||||||
|
PackageFileOwnershipOverlap bool `mapstructure:"package-file-ownership-overlap" json:"package-file-ownership-overlap" yaml:"package-file-ownership-overlap"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultRelationshipsConfig() relationshipsConfig {
|
||||||
|
return relationshipsConfig{
|
||||||
|
PackageFileOwnership: true,
|
||||||
|
PackageFileOwnershipOverlap: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *relationshipsConfig) DescribeFields(descriptions fangs.FieldDescriptionSet) {
|
||||||
|
descriptions.Add(&r.PackageFileOwnership, "include package-to-file relationships that indicate which files are owned by which packages.")
|
||||||
|
descriptions.Add(&r.PackageFileOwnershipOverlap, "include package-to-package relationships that indicate one package is owned by another due to files claimed to be owned by one package are also evidence of another package's existence.")
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package options
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RootOptions struct {
|
|
||||||
Config string
|
|
||||||
Quiet bool
|
|
||||||
Verbose int
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Interface = (*RootOptions)(nil)
|
|
||||||
|
|
||||||
func (o *RootOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
|
|
||||||
cmd.PersistentFlags().StringVarP(&o.Config, "config", "c", "", "application config file")
|
|
||||||
cmd.PersistentFlags().CountVarP(&o.Verbose, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
|
|
||||||
cmd.PersistentFlags().BoolVarP(&o.Quiet, "quiet", "q", false, "suppress all logging output")
|
|
||||||
|
|
||||||
return bindRootConfigOptions(cmd.PersistentFlags(), v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindRootConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
|
|
||||||
if err := v.BindPFlag("config", flags.Lookup("config")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := v.BindPFlag("verbosity", flags.Lookup("verbose")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := v.BindPFlag("quiet", flags.Lookup("quiet")); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/clio"
|
||||||
|
"github.com/anchore/syft/internal/redact"
|
||||||
|
)
|
||||||
|
|
||||||
|
type secret string
|
||||||
|
|
||||||
|
var _ interface {
|
||||||
|
fmt.Stringer
|
||||||
|
clio.PostLoader
|
||||||
|
} = (*secret)(nil)
|
||||||
|
|
||||||
|
// PostLoad needs to use a pointer receiver, even if it's not modifying the value
|
||||||
|
func (r *secret) PostLoad() error {
|
||||||
|
redact.Add(string(*r))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r secret) String() string {
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package options
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/scylladb/go-set/strset"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sourceConfig struct {
|
||||||
|
Name string `json:"name" yaml:"name" mapstructure:"name"`
|
||||||
|
Version string `json:"version" yaml:"version" mapstructure:"version"`
|
||||||
|
BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths
|
||||||
|
File fileSource `json:"file" yaml:"file" mapstructure:"file"`
|
||||||
|
Image imageSource `json:"image" yaml:"image" mapstructure:"image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileSource struct {
|
||||||
|
Digests []string `json:"digests" yaml:"digests" mapstructure:"digests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageSource struct {
|
||||||
|
DefaultPullSource string `json:"default-pull-source" yaml:"default-pull-source" mapstructure:"default-pull-source"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultSourceConfig() sourceConfig {
|
||||||
|
return sourceConfig{
|
||||||
|
File: fileSource{
|
||||||
|
Digests: []string{"sha256"},
|
||||||
|
},
|
||||||
|
Image: imageSource{
|
||||||
|
DefaultPullSource: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c imageSource) PostLoad() error {
|
||||||
|
return checkDefaultSourceValues(c.DefaultPullSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
var validDefaultSourceValues = []string{"registry", "docker", "podman", ""}
|
||||||
|
|
||||||
|
func checkDefaultSourceValues(source string) error {
|
||||||
|
validValues := strset.New(validDefaultSourceValues...)
|
||||||
|
if !validValues.Has(source) {
|
||||||
|
validValuesString := strings.Join(validDefaultSourceValues, ", ")
|
||||||
|
return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user