mirror of
https://github.com/go-task/task.git
synced 2025-06-10 23:57:30 +02:00
feat: checksum pinning
This commit is contained in:
parent
d47bad9071
commit
3daaf92d4c
@ -26,6 +26,7 @@ const (
|
|||||||
CodeTaskfileNetworkTimeout
|
CodeTaskfileNetworkTimeout
|
||||||
CodeTaskfileInvalid
|
CodeTaskfileInvalid
|
||||||
CodeTaskfileCycle
|
CodeTaskfileCycle
|
||||||
|
CodeTaskfileDoesNotMatchChecksum
|
||||||
)
|
)
|
||||||
|
|
||||||
// Task related exit codes
|
// Task related exit codes
|
||||||
|
@ -187,3 +187,24 @@ func (err TaskfileCycleError) Error() string {
|
|||||||
func (err TaskfileCycleError) Code() int {
|
func (err TaskfileCycleError) Code() int {
|
||||||
return CodeTaskfileCycle
|
return CodeTaskfileCycle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TaskfileDoesNotMatchChecksum is returned when a Taskfile's checksum does not
|
||||||
|
// match the one pinned in the parent Taskfile.
|
||||||
|
type TaskfileDoesNotMatchChecksum struct {
|
||||||
|
URI string
|
||||||
|
ExpectedChecksum string
|
||||||
|
ActualChecksum string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *TaskfileDoesNotMatchChecksum) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
|
||||||
|
err.URI,
|
||||||
|
err.ActualChecksum,
|
||||||
|
err.ExpectedChecksum,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *TaskfileDoesNotMatchChecksum) Code() int {
|
||||||
|
return CodeTaskfileDoesNotMatchChecksum
|
||||||
|
}
|
||||||
|
@ -958,3 +958,23 @@ func TestFuzzyModel(t *testing.T) {
|
|||||||
WithTask("install"),
|
WithTask("install"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIncludeChecksum(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
NewExecutorTest(t,
|
||||||
|
WithName("correct"),
|
||||||
|
WithExecutorOptions(
|
||||||
|
task.WithDir("testdata/includes_checksum/correct"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
NewExecutorTest(t,
|
||||||
|
WithName("incorrect"),
|
||||||
|
WithExecutorOptions(
|
||||||
|
task.WithDir("testdata/includes_checksum/incorrect"),
|
||||||
|
),
|
||||||
|
WithSetupError(),
|
||||||
|
WithPostProcessFn(PPRemoveAbsolutePaths),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -24,6 +24,7 @@ type (
|
|||||||
AdvancedImport bool
|
AdvancedImport bool
|
||||||
Vars *Vars
|
Vars *Vars
|
||||||
Flatten bool
|
Flatten bool
|
||||||
|
Checksum string
|
||||||
}
|
}
|
||||||
// Includes is an ordered map of namespaces to includes.
|
// Includes is an ordered map of namespaces to includes.
|
||||||
Includes struct {
|
Includes struct {
|
||||||
@ -165,6 +166,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
|||||||
Aliases []string
|
Aliases []string
|
||||||
Excludes []string
|
Excludes []string
|
||||||
Vars *Vars
|
Vars *Vars
|
||||||
|
Checksum string
|
||||||
}
|
}
|
||||||
if err := node.Decode(&includedTaskfile); err != nil {
|
if err := node.Decode(&includedTaskfile); err != nil {
|
||||||
return errors.NewTaskfileDecodeError(err, node)
|
return errors.NewTaskfileDecodeError(err, node)
|
||||||
@ -178,6 +180,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
|||||||
include.AdvancedImport = true
|
include.AdvancedImport = true
|
||||||
include.Vars = includedTaskfile.Vars
|
include.Vars = includedTaskfile.Vars
|
||||||
include.Flatten = includedTaskfile.Flatten
|
include.Flatten = includedTaskfile.Flatten
|
||||||
|
include.Checksum = includedTaskfile.Checksum
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,5 +203,7 @@ func (include *Include) DeepCopy() *Include {
|
|||||||
AdvancedImport: include.AdvancedImport,
|
AdvancedImport: include.AdvancedImport,
|
||||||
Vars: include.Vars.DeepCopy(),
|
Vars: include.Vars.DeepCopy(),
|
||||||
Flatten: include.Flatten,
|
Flatten: include.Flatten,
|
||||||
|
Aliases: deepcopy.Slice(include.Aliases),
|
||||||
|
Checksum: include.Checksum,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@ type Node interface {
|
|||||||
Parent() Node
|
Parent() Node
|
||||||
Location() string
|
Location() string
|
||||||
Dir() string
|
Dir() string
|
||||||
|
Checksum() string
|
||||||
|
Verify(checksum string) bool
|
||||||
ResolveEntrypoint(entrypoint string) (string, error)
|
ResolveEntrypoint(entrypoint string) (string, error)
|
||||||
ResolveDir(dir string) (string, error)
|
ResolveDir(dir string) (string, error)
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ type (
|
|||||||
baseNode struct {
|
baseNode struct {
|
||||||
parent Node
|
parent Node
|
||||||
dir string
|
dir string
|
||||||
|
checksum string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -32,6 +33,12 @@ func WithParent(parent Node) NodeOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithChecksum(checksum string) NodeOption {
|
||||||
|
return func(node *baseNode) {
|
||||||
|
node.checksum = checksum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (node *baseNode) Parent() Node {
|
func (node *baseNode) Parent() Node {
|
||||||
return node.parent
|
return node.parent
|
||||||
}
|
}
|
||||||
@ -39,3 +46,11 @@ func (node *baseNode) Parent() Node {
|
|||||||
func (node *baseNode) Dir() string {
|
func (node *baseNode) Dir() string {
|
||||||
return node.dir
|
return node.dir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (node *baseNode) Checksum() string {
|
||||||
|
return node.checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *baseNode) Verify(checksum string) bool {
|
||||||
|
return node.checksum == "" || node.checksum == checksum
|
||||||
|
}
|
||||||
|
@ -250,6 +250,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
|
|||||||
AdvancedImport: include.AdvancedImport,
|
AdvancedImport: include.AdvancedImport,
|
||||||
Excludes: include.Excludes,
|
Excludes: include.Excludes,
|
||||||
Vars: include.Vars,
|
Vars: include.Vars,
|
||||||
|
Checksum: include.Checksum,
|
||||||
}
|
}
|
||||||
if err := cache.Err(); err != nil {
|
if err := cache.Err(); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -267,6 +268,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
|
|||||||
|
|
||||||
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
|
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
|
||||||
WithParent(node),
|
WithParent(node),
|
||||||
|
WithChecksum(include.Checksum),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if include.Optional {
|
if include.Optional {
|
||||||
@ -362,7 +364,24 @@ func (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error)
|
|||||||
if node, isRemote := node.(RemoteNode); isRemote {
|
if node, isRemote := node.(RemoteNode); isRemote {
|
||||||
return r.readRemoteNodeContent(ctx, node)
|
return r.readRemoteNodeContent(ctx, node)
|
||||||
}
|
}
|
||||||
return node.Read()
|
|
||||||
|
// Read the Taskfile
|
||||||
|
b, err := node.Read()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the given checksum doesn't match the sum pinned in the Taskfile
|
||||||
|
checksum := checksum(b)
|
||||||
|
if !node.Verify(checksum) {
|
||||||
|
return nil, &errors.TaskfileDoesNotMatchChecksum{
|
||||||
|
URI: node.Location(),
|
||||||
|
ExpectedChecksum: node.Checksum(),
|
||||||
|
ActualChecksum: checksum,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) {
|
func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) {
|
||||||
@ -427,10 +446,21 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.debugf("found remote file at %q\n", node.Location())
|
r.debugf("found remote file at %q\n", node.Location())
|
||||||
checksum := checksum(downloadedBytes)
|
|
||||||
prompt := cache.ChecksumPrompt(checksum)
|
|
||||||
|
|
||||||
|
// If the given checksum doesn't match the sum pinned in the Taskfile
|
||||||
|
checksum := checksum(downloadedBytes)
|
||||||
|
if !node.Verify(checksum) {
|
||||||
|
return nil, &errors.TaskfileDoesNotMatchChecksum{
|
||||||
|
URI: node.Location(),
|
||||||
|
ExpectedChecksum: node.Checksum(),
|
||||||
|
ActualChecksum: checksum,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is no manual checksum pin, run the automatic checks
|
||||||
|
if node.Checksum() == "" {
|
||||||
// Prompt the user if required
|
// Prompt the user if required
|
||||||
|
prompt := cache.ChecksumPrompt(checksum)
|
||||||
if prompt != "" {
|
if prompt != "" {
|
||||||
if err := func() error {
|
if err := func() error {
|
||||||
r.promptMutex.Lock()
|
r.promptMutex.Lock()
|
||||||
@ -440,6 +470,7 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
|
|||||||
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
|
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store the checksum
|
// Store the checksum
|
||||||
if err := cache.WriteChecksum(checksum); err != nil {
|
if err := cache.WriteChecksum(checksum); err != nil {
|
||||||
|
12
testdata/includes_checksum/correct/Taskfile.yml
vendored
Normal file
12
testdata/includes_checksum/correct/Taskfile.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
included:
|
||||||
|
taskfile: ../included.yml
|
||||||
|
internal: true
|
||||||
|
checksum: c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
cmds:
|
||||||
|
- task: included:default
|
2
testdata/includes_checksum/correct/testdata/TestIncludeChecksum-correct.golden
vendored
Normal file
2
testdata/includes_checksum/correct/testdata/TestIncludeChecksum-correct.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
task: [included:default] echo "Hello, World!"
|
||||||
|
Hello, World!
|
12
testdata/includes_checksum/correct_remote/Taskfile.yml
vendored
Normal file
12
testdata/includes_checksum/correct_remote/Taskfile.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
included:
|
||||||
|
taskfile: https://taskfile.dev
|
||||||
|
internal: true
|
||||||
|
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
cmds:
|
||||||
|
- task: included:default
|
6
testdata/includes_checksum/included.yml
vendored
Normal file
6
testdata/includes_checksum/included.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
cmds:
|
||||||
|
- echo "Hello, World!"
|
12
testdata/includes_checksum/incorrect/Taskfile.yml
vendored
Normal file
12
testdata/includes_checksum/incorrect/Taskfile.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
included:
|
||||||
|
taskfile: ../included.yml
|
||||||
|
internal: true
|
||||||
|
checksum: foo
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
cmds:
|
||||||
|
- task: included:default
|
3
testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden
vendored
Normal file
3
testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
task: The checksum of the Taskfile at "/testdata/includes_checksum/included.yml" does not match!
|
||||||
|
got: "c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5"
|
||||||
|
want: "foo"
|
0
testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect.golden
vendored
Normal file
0
testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect.golden
vendored
Normal file
@ -182,9 +182,11 @@ includes:
|
|||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
### Automatic checksums
|
||||||
|
|
||||||
Running commands from sources that you do not control is always a potential
|
Running commands from sources that you do not control is always a potential
|
||||||
security risk. For this reason, we have added some checks when using remote
|
security risk. For this reason, we have added some automatic checks when using
|
||||||
Taskfiles:
|
remote Taskfiles:
|
||||||
|
|
||||||
1. When running a task from a remote Taskfile for the first time, Task will
|
1. When running a task from a remote Taskfile for the first time, Task will
|
||||||
print a warning to the console asking you to check that you are sure that you
|
print a warning to the console asking you to check that you are sure that you
|
||||||
@ -209,6 +211,38 @@ flag. Before enabling this flag, you should:
|
|||||||
containing a commit hash) to prevent Task from automatically accepting a
|
containing a commit hash) to prevent Task from automatically accepting a
|
||||||
prompt that says a remote Taskfile has changed.
|
prompt that says a remote Taskfile has changed.
|
||||||
|
|
||||||
|
### Manual checksum pinning
|
||||||
|
|
||||||
|
Alternatively, if you expect the contents of your remote files to be a constant
|
||||||
|
value, you can pin the checksum of the included file instead:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
includes:
|
||||||
|
included:
|
||||||
|
taskfile: https://taskfile.dev
|
||||||
|
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
|
||||||
|
```
|
||||||
|
|
||||||
|
This will disable the automatic checksum prompts discussed above. However, if
|
||||||
|
the checksums do not match, Task will exit immediately with an error. When
|
||||||
|
setting this up for the first time, you may not know the correct value of the
|
||||||
|
checksum. There are a couple of ways you can obtain this:
|
||||||
|
|
||||||
|
1. Add the include normally without the `checksum` key. The first time you run
|
||||||
|
the included Taskfile, a `.task/remote` temporary directory is created. Find
|
||||||
|
the correct set of files for your included Taskfile and open the file that
|
||||||
|
ends with `.checksum`. You can copy the contents of this file and paste it
|
||||||
|
into the `checksum` key of your include. This method is safest as it allows
|
||||||
|
you to inspect the downloaded Taskfile before you pin it.
|
||||||
|
2. Alternatively, add the include with a temporary random value in the
|
||||||
|
`checksum` key. When you try to run the Taskfile, you will get an error that
|
||||||
|
will report the incorrect expected checksum and the actual checksum. You can
|
||||||
|
copy the actual checksum and replace your temporary random value.
|
||||||
|
|
||||||
|
### TLS
|
||||||
|
|
||||||
Task currently supports both `http` and `https` URLs. However, the `http`
|
Task currently supports both `http` and `https` URLs. However, the `http`
|
||||||
requests will not execute by default unless you run the task with the
|
requests will not execute by default unless you run the task with the
|
||||||
`--insecure` flag. This is to protect you from accidentally running a remote
|
`--insecure` flag. This is to protect you from accidentally running a remote
|
||||||
|
@ -34,6 +34,7 @@ toc_max_heading_level: 5
|
|||||||
| `internal` | `bool` | `false` | Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`. |
|
| `internal` | `bool` | `false` | Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`. |
|
||||||
| `aliases` | `[]string` | | Alternative names for the namespace of the included Taskfile. |
|
| `aliases` | `[]string` | | Alternative names for the namespace of the included Taskfile. |
|
||||||
| `vars` | `map[string]Variable` | | A set of variables to apply to the included Taskfile. |
|
| `vars` | `map[string]Variable` | | A set of variables to apply to the included Taskfile. |
|
||||||
|
| `checksum` | `string` | | The checksum of the file you expect to include. If the checksum does not match, the file will not be included. |
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|
||||||
|
@ -684,6 +684,10 @@
|
|||||||
"vars": {
|
"vars": {
|
||||||
"description": "A set of variables to apply to the included Taskfile.",
|
"description": "A set of variables to apply to the included Taskfile.",
|
||||||
"$ref": "#/definitions/vars"
|
"$ref": "#/definitions/vars"
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"description": "The checksum of the file you expect to include. If the checksum does not match, the file will not be included.",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user