pipedawg-vl 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +113 -0
- data/lib/pipedawg/job/helm/copy.rb +106 -0
- data/lib/pipedawg/job/helm.rb +16 -0
- data/lib/pipedawg/job/kaniko/build.rb +98 -0
- data/lib/pipedawg/job/kaniko.rb +16 -0
- data/lib/pipedawg/job/qualys/scan.rb +125 -0
- data/lib/pipedawg/job/qualys.rb +12 -0
- data/lib/pipedawg/job/skopeo/copy.rb +77 -0
- data/lib/pipedawg/job/skopeo.rb +16 -0
- data/lib/pipedawg/job.rb +40 -0
- data/lib/pipedawg/pipeline.rb +52 -0
- data/lib/pipedawg/util.rb +44 -0
- data/lib/pipedawg/version.rb +5 -0
- data/lib/pipedawg.rb +19 -0
- metadata +61 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 96aac89e278239ef899ad613b712eea25f80c2029e1769ca32420a8bd4f03976
|
4
|
+
data.tar.gz: a233bd484f256cffe03c7cb1f0bfdcb774870820e83f0091169366edb3e252c8
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 165b090587e34fb4a7ca5c9c8768ab66111b0cc9898dbf1aa51c482f237b6b3d27c81cc76471d931632961cfd568ba50626d0280a1a28937d1de3aac78cf5cc1
|
7
|
+
data.tar.gz: 7a97e66e61163d11fc9856a52504429a2ed96981b88d1f7d23753c5e86a5a11757a35ed93eb12082e60a8786b01595a329568ac7a43eba72645ee77aa81e6ba0
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2022 Richard Grainger
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
# pipedawg
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/pipedawg)
|
4
|
+
|
5
|
+
Generate GitLab CI pipelines.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Install `pipedawg` with:
|
10
|
+
|
11
|
+
```
|
12
|
+
gem install pipedawg
|
13
|
+
```
|
14
|
+
|
15
|
+
Or add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'pipedawg'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
```sh
|
24
|
+
bundle install
|
25
|
+
```
|
26
|
+
|
27
|
+
## Ruby library
|
28
|
+
|
29
|
+
Example:
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
#!/usr/bin/env ruby
|
33
|
+
# frozen_string_literal: true
|
34
|
+
|
35
|
+
require 'pipedawg'
|
36
|
+
|
37
|
+
gem_job = Pipedawg::Job.new(
|
38
|
+
'build:gem',
|
39
|
+
artifacts: ['*.gem'],
|
40
|
+
image: 'ruby',
|
41
|
+
script: ['bundle install', 'gem build *.gemspec']
|
42
|
+
)
|
43
|
+
|
44
|
+
kaniko_build_job = Pipedawg::Job::Kaniko::Build.new(
|
45
|
+
'build:kaniko',
|
46
|
+
needs: ['build:gem'],
|
47
|
+
retry: 2,
|
48
|
+
context:'${CI_PROJECT_DIR}/docker',
|
49
|
+
external_files: {'*.gem':'gems'},
|
50
|
+
debug: false
|
51
|
+
)
|
52
|
+
|
53
|
+
pipeline = Pipedawg::Pipeline.new 'build:image', jobs: [gem_job, kaniko_build_job]
|
54
|
+
puts pipeline.to_yaml
|
55
|
+
pipeline.to_yaml_file('/tmp/pipeline.yaml')
|
56
|
+
```
|
57
|
+
|
58
|
+
```console
|
59
|
+
$ cat /tmp/pipeline.yaml
|
60
|
+
---
|
61
|
+
stages:
|
62
|
+
- '1'
|
63
|
+
- '2'
|
64
|
+
build:gem:
|
65
|
+
artifacts:
|
66
|
+
- "*.gem"
|
67
|
+
cache: {}
|
68
|
+
image: ruby
|
69
|
+
needs: []
|
70
|
+
script:
|
71
|
+
- bundle install
|
72
|
+
- gem build *.gemspec
|
73
|
+
stage: '1'
|
74
|
+
tags: []
|
75
|
+
build:kaniko:
|
76
|
+
artifacts: {}
|
77
|
+
cache: {}
|
78
|
+
image:
|
79
|
+
entrypoint:
|
80
|
+
- ''
|
81
|
+
name: gcr.io/kaniko-project/executor:debug
|
82
|
+
needs:
|
83
|
+
- build:gem
|
84
|
+
retry: 2
|
85
|
+
script:
|
86
|
+
- echo "{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}"
|
87
|
+
> "/kaniko/.docker/config.json"
|
88
|
+
- cp "*.gem" "${CI_PROJECT_DIR}/docker/gems"
|
89
|
+
- '"/kaniko/executor" --context "${CI_PROJECT_DIR}/docker" --dockerfile "Dockerfile"
|
90
|
+
--destination ${CI_REGISTRY_IMAGE}:latest'
|
91
|
+
stage: '2'
|
92
|
+
tags: []
|
93
|
+
```
|
94
|
+
|
95
|
+
## Development
|
96
|
+
|
97
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. Run `bundle exec rubocop` to run the linter. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
98
|
+
|
99
|
+
Note that by default, Bundler will attempt to install gems to the system, e.g. `/usr/bin`, `/usr/share`, which requires elevated access and can interfere with files that are managed by the system's package manager. This behaviour can be overridden by creating the file `.bundle/config` and adding the following line:
|
100
|
+
```
|
101
|
+
BUNDLE_PATH: "./.bundle"
|
102
|
+
```
|
103
|
+
When you run `bin/setup` or `bundle install`, all gems will be installed inside the .bundle directory of this project.
|
104
|
+
|
105
|
+
To make this behaviour a default for all gem projects, the above line can be added to the user's bundle config file in their home directory (`~/.bundle/config`)
|
106
|
+
|
107
|
+
## Contributing
|
108
|
+
|
109
|
+
Bug reports and pull requests are welcome on [GitHub](https://github.com/ValdrinLushaj/pipedawg).
|
110
|
+
|
111
|
+
## License
|
112
|
+
|
113
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
class Job
|
5
|
+
class Helm
|
6
|
+
# Pipedawg::Job::Helm::Copy class
|
7
|
+
class Copy < Job::Helm
|
8
|
+
def initialize(name, opts = {})
|
9
|
+
opts = {
|
10
|
+
chart: name,
|
11
|
+
destinations: [{ user: nil, password: nil, url: nil }],
|
12
|
+
password: nil, url: nil, user: nil, version: nil
|
13
|
+
}.merge(opts)
|
14
|
+
super name, opts
|
15
|
+
update
|
16
|
+
end
|
17
|
+
|
18
|
+
def update
|
19
|
+
opts[:script] = debug + pull + (opts[:destinations].map { |d| push(d) }).flatten(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def pull
|
25
|
+
case opts[:url]
|
26
|
+
when nil
|
27
|
+
[]
|
28
|
+
when %r{^oci://}
|
29
|
+
pull_oci
|
30
|
+
else
|
31
|
+
pull_classic
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def push(destination)
|
36
|
+
case destination[:url]
|
37
|
+
when nil
|
38
|
+
[]
|
39
|
+
when %r{^oci://}
|
40
|
+
push_oci(destination)
|
41
|
+
else
|
42
|
+
push_classic(destination)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def pull_oci # rubocop:disable Metrics/AbcSize
|
47
|
+
script = []
|
48
|
+
if opts[:url] && opts[:chart] && opts[:version]
|
49
|
+
script = ['export HELM_EXPERIMENTAL_OCI=1']
|
50
|
+
script << login_oci(opts) if opts[:user] && opts[:password]
|
51
|
+
script << "\"#{opts[:command]}\" pull \"#{opts[:url]}/#{opts[:chart]}\" --version \"#{opts[:version]}\""
|
52
|
+
end
|
53
|
+
script
|
54
|
+
end
|
55
|
+
|
56
|
+
def push_oci(destination) # rubocop:disable Metrics/AbcSize
|
57
|
+
script = []
|
58
|
+
if destination[:url] && opts[:chart] && opts[:version]
|
59
|
+
script = ['export HELM_EXPERIMENTAL_OCI=1']
|
60
|
+
script << login_oci(destination) if destination[:user] && destination[:password]
|
61
|
+
script << "\"#{opts[:command]}\" push \"#{opts[:chart]}-#{opts[:version]}.tgz\" \"#{destination[:url]}\""
|
62
|
+
end
|
63
|
+
script
|
64
|
+
end
|
65
|
+
|
66
|
+
def login_oci(login_opts)
|
67
|
+
require 'uri'
|
68
|
+
"echo \"#{login_opts[:password]}\" | \"#{opts[:command]}\" registry login --username \"#{login_opts[:user]}\" --password-stdin \"#{URI(login_opts[:url]).host}\"" # rubocop:disable Layout/LineLength
|
69
|
+
end
|
70
|
+
|
71
|
+
def pull_classic # rubocop:disable Metrics/AbcSize
|
72
|
+
script = []
|
73
|
+
if opts[:url] && opts[:chart] && opts[:version]
|
74
|
+
suffix = login_classic(opts)
|
75
|
+
script << "\"#{opts[:command]}\" repo add source \"#{opts[:url]}\"#{suffix}"
|
76
|
+
script << "\"#{opts[:command]}\" repo update"
|
77
|
+
script << "\"#{opts[:command]}\" pull \"source/#{opts[:chart]}\" --version \"#{opts[:version]}\""
|
78
|
+
end
|
79
|
+
script
|
80
|
+
end
|
81
|
+
|
82
|
+
def push_classic(destination)
|
83
|
+
script = []
|
84
|
+
if destination[:url] && opts[:chart] && opts[:version]
|
85
|
+
script << plugin_classic
|
86
|
+
suffix = login_classic(destination)
|
87
|
+
script << "\"#{opts[:command]}\" cm-push \"#{opts[:chart]}-#{opts[:version]}.tgz\" \"#{destination[:url]}\"#{suffix}" # rubocop:disable Layout/LineLength
|
88
|
+
end
|
89
|
+
script
|
90
|
+
end
|
91
|
+
|
92
|
+
def login_classic(login_opts)
|
93
|
+
if login_opts[:user] && login_opts[:password]
|
94
|
+
" --username \"#{login_opts[:user]}\" --password \"#{login_opts[:password]}\""
|
95
|
+
else
|
96
|
+
''
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def plugin_classic
|
101
|
+
"\"#{opts[:command]}\" plugin list | grep -q cm-push || \"#{opts[:command]}\" plugin install https://github.com/chartmuseum/helm-push"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
class Job
|
5
|
+
# Pipedawg::Job::Helm class
|
6
|
+
class Helm < Job
|
7
|
+
def initialize(name, opts = {})
|
8
|
+
opts = {
|
9
|
+
command: 'helm',
|
10
|
+
image: { entrypoint: [''], name: 'alpine/helm' }
|
11
|
+
}.merge(opts)
|
12
|
+
super name, opts
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
class Job
|
5
|
+
class Kaniko
|
6
|
+
# Pipedawg::Job::Kaniko::Build class
|
7
|
+
class Build < Job::Kaniko
|
8
|
+
def initialize(name, opts = {}) # rubocop:disable Metrics/MethodLength
|
9
|
+
opts = {
|
10
|
+
build_args: {},
|
11
|
+
config: { auths: { '$CI_REGISTRY': { username: '$CI_REGISTRY_USER', password: '$CI_REGISTRY_PASSWORD' } } },
|
12
|
+
config_file: '/kaniko/.docker/config.json', context: '${CI_PROJECT_DIR}',
|
13
|
+
destinations: ['${CI_REGISTRY_IMAGE}:latest'], dockerfile: 'Dockerfile', external_files: {}, flags: [],
|
14
|
+
ignore_paths: [], insecure_registries: [], options: {}, registry_certificates: {}, registry_mirrors: [],
|
15
|
+
skip_tls_verify_registry: [], trusted_ca_cert_source_files: [],
|
16
|
+
trusted_ca_cert_target_file: '/kaniko/ssl/certs/ca-certificates.crt'
|
17
|
+
}.merge(opts)
|
18
|
+
super name, opts
|
19
|
+
update
|
20
|
+
end
|
21
|
+
|
22
|
+
def update
|
23
|
+
require 'json'
|
24
|
+
opts[:script] = debug + config + cert_copies + file_copies + Array(kaniko_cmd)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def config
|
30
|
+
["echo #{opts[:config].to_json.inspect} > \"#{opts[:config_file]}\""]
|
31
|
+
end
|
32
|
+
|
33
|
+
def cert_copies
|
34
|
+
Array(opts[:trusted_ca_cert_source_files]).map do |cert|
|
35
|
+
"cat \"#{cert}\" >> \"#{opts[:trusted_ca_cert_target_file]}\""
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def file_copies
|
40
|
+
opts[:external_files].map do |source, dest|
|
41
|
+
"cp \"#{source}\" \"#{opts[:context]}/#{dest}\""
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def kaniko_cmd # rubocop:disable Metrics/AbcSize
|
46
|
+
["\"#{opts[:command]}\" --context \"#{opts[:context]}\"",
|
47
|
+
"--dockerfile \"#{opts[:dockerfile]}\"", flags, options, build_args,
|
48
|
+
ignore_paths, insecure_registries, registry_certificates, registry_mirrors,
|
49
|
+
destinations, skip_tls_verify_registries].reject(&:empty?).join(' ')
|
50
|
+
end
|
51
|
+
|
52
|
+
def flags
|
53
|
+
flags = opts[:flags].clone
|
54
|
+
flags << 'no-push' if opts[:destinations].empty?
|
55
|
+
flags.uniq.map { |f| "--#{f}" }.join(' ')
|
56
|
+
end
|
57
|
+
|
58
|
+
def options
|
59
|
+
opts[:options].map { |k, v| "--#{k}=\"#{v}\"" }.join(' ')
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_args
|
63
|
+
opts[:build_args].map { |k, v| "--build-arg #{k}=\"#{v}\"" }.join(' ')
|
64
|
+
end
|
65
|
+
|
66
|
+
def ignore_paths
|
67
|
+
Array(opts[:ignore_paths]).map { |p| "--ignore-path #{p}" }.join(' ')
|
68
|
+
end
|
69
|
+
|
70
|
+
def insecure_registries
|
71
|
+
Array(opts[:insecure_registries]).map do |r|
|
72
|
+
"--insecure-registry #{r}"
|
73
|
+
end.join(' ')
|
74
|
+
end
|
75
|
+
|
76
|
+
def registry_certificates
|
77
|
+
opts[:registry_certificates].map do |k, v|
|
78
|
+
"--registry-certificate #{k}=\"#{v}\""
|
79
|
+
end.join(' ')
|
80
|
+
end
|
81
|
+
|
82
|
+
def registry_mirrors
|
83
|
+
Array(opts[:registry_mirrors]).map { |r| "--registry-mirror #{r}" }.join(' ')
|
84
|
+
end
|
85
|
+
|
86
|
+
def destinations
|
87
|
+
opts[:destinations].map { |d| "--destination #{d}" }.join(' ')
|
88
|
+
end
|
89
|
+
|
90
|
+
def skip_tls_verify_registries
|
91
|
+
Array(opts[:skip_tls_verify_registry]).map do |r|
|
92
|
+
"--skip-tls-verify-registry #{r}"
|
93
|
+
end.join(' ')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
class Job
|
5
|
+
# Pipedawg::Job::Kaniko class
|
6
|
+
class Kaniko < Job
|
7
|
+
def initialize(name, opts = {})
|
8
|
+
opts = {
|
9
|
+
command: '/kaniko/executor',
|
10
|
+
image: { entrypoint: [''], name: 'gcr.io/kaniko-project/executor:debug' }
|
11
|
+
}.merge(opts)
|
12
|
+
super name, opts
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
class Job
|
5
|
+
class Qualys
|
6
|
+
# Pipedawg::Job::Qualys::Scan class
|
7
|
+
class Scan < Job::Qualys # rubocop:disable Metrics/ClassLength
|
8
|
+
def initialize(name, opts = {})
|
9
|
+
opts = {
|
10
|
+
acceptable_risk: '${QUALYS_ACCEPTABLE_IMAGE_RISK}',
|
11
|
+
artifacts: { expire_in: '1 month', paths: ['software.json', 'vulnerabilities.json'], when: 'always' },
|
12
|
+
config: { auths: { '$CI_REGISTRY': { username: '$CI_REGISTRY_USER', password: '$CI_REGISTRY_PASSWORD' } } },
|
13
|
+
gateway: '${QUALYS_GATEWAY}', image: nil, password: '${QUALYS_PASSWORD}',
|
14
|
+
scan_image: '${QUALYS_IMAGE}', scan_target_prefix: 'qualys_scan_target',
|
15
|
+
user: '${QUALYS_USERNAME}', variables: { GIT_STRATEGY: 'clone' }
|
16
|
+
}.merge(opts)
|
17
|
+
super name, opts
|
18
|
+
update
|
19
|
+
end
|
20
|
+
|
21
|
+
def update # rubocop:disable Metrics/AbcSize
|
22
|
+
require 'json'
|
23
|
+
opts[:script] =
|
24
|
+
debug + config + image + clean_config + token + scan_start +
|
25
|
+
scan_complete + artifacts + severities + outputs
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def debug # rubocop:disable Metrics/MethodLength
|
31
|
+
if opts[:debug]
|
32
|
+
super + [
|
33
|
+
'echo Qualys settings:', "echo Qualys gateway: \"#{opts[:gateway]}\"",
|
34
|
+
"echo Qualys username: \"#{opts[:user]}\"",
|
35
|
+
"if [ \"#{opts[:password]}\" != '' ]; then " \
|
36
|
+
'echo Qualys password is not empty; else ' \
|
37
|
+
'echo Qualys password is not set; exit 1; fi'
|
38
|
+
]
|
39
|
+
else
|
40
|
+
[]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def config
|
45
|
+
['export CONFIG=$(mktemp -d)', "echo #{opts[:config].to_json.inspect} > \"${CONFIG}/config.json\""]
|
46
|
+
end
|
47
|
+
|
48
|
+
def image
|
49
|
+
[
|
50
|
+
"image_target=\"#{opts[:scan_target_prefix]}:$(echo #{opts[:scan_image]} | sed 's/^[^/]*\\///'| sed 's/[:/]/-/g')\"", # rubocop:disable Layout/LineLength
|
51
|
+
"docker --config=\"${CONFIG}\" pull \"#{opts[:scan_image]}\"",
|
52
|
+
"docker image tag \"#{opts[:scan_image]}\" \"${image_target}\"",
|
53
|
+
"image_id=$(docker inspect --format=\"{{index .Id}}\" \"#{opts[:scan_image]}\" | cut -c8-19)",
|
54
|
+
'echo "Image ID: ${image_id}"'
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
def clean_config
|
59
|
+
[
|
60
|
+
'rm -f "${CONFIG}/config.json"',
|
61
|
+
'rmdir "${CONFIG}"'
|
62
|
+
]
|
63
|
+
end
|
64
|
+
|
65
|
+
def token
|
66
|
+
["token=$(curl -s --location --request POST \"https://#{opts[:gateway]}/auth\" --header \"Content-Type: application/x-www-form-urlencoded\" --data-urlencode \"username=#{opts[:user]}\" --data-urlencode \"password=#{opts[:password]}\" --data-urlencode \"token=true\")"] # rubocop:disable Layout/LineLength
|
67
|
+
end
|
68
|
+
|
69
|
+
def scan_start
|
70
|
+
[
|
71
|
+
'while true; do ' \
|
72
|
+
"result=$(curl -s -o /dev/null -w ''%{http_code}'' --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id\" --header \"Authorization: Bearer $token\"); " + # rubocop:disable Layout/LineLength, Style/FormatStringToken
|
73
|
+
'echo "Waiting for scan to start..."; ' \
|
74
|
+
'echo " Result: ${result}"; ' \
|
75
|
+
'if [ "${result}" = "200" ]; then break; fi; ' \
|
76
|
+
'sleep 10; done'
|
77
|
+
]
|
78
|
+
end
|
79
|
+
|
80
|
+
def scan_complete
|
81
|
+
[
|
82
|
+
'while true; do ' \
|
83
|
+
"result=$(curl -s --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id\" --header \"Authorization: Bearer $token\" | jq -r '.scanStatus'); " + # rubocop:disable Layout/LineLength
|
84
|
+
'echo "Waiting for scan to complete..."; ' \
|
85
|
+
'echo " Result: ${result}"; ' \
|
86
|
+
'if [ "${result}" = "SUCCESS" ]; then break; fi; ' \
|
87
|
+
'sleep 10; done; sleep 30'
|
88
|
+
]
|
89
|
+
end
|
90
|
+
|
91
|
+
def artifacts
|
92
|
+
[
|
93
|
+
"curl -s --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id/software\" --header \"Authorization: Bearer $token\" | jq . > software.json", # rubocop:disable Layout/LineLength
|
94
|
+
"curl -s --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id/vuln\" --header \"Authorization: Bearer $token\" | jq . > vulnerabilities.json" # rubocop:disable Layout/LineLength
|
95
|
+
]
|
96
|
+
end
|
97
|
+
|
98
|
+
def severities
|
99
|
+
[
|
100
|
+
"response=$(curl -s --location --request GET \"https://#{opts[:gateway]}/csapi/v1.2/images/$image_id/vuln/count\" --header \"Authorization: Bearer $token\")", # rubocop:disable Layout/LineLength
|
101
|
+
'severity5=$(jq -r ".severity5Count" <<< "${response}")',
|
102
|
+
'severity4=$(jq -r ".severity4Count" <<< "${response}")'
|
103
|
+
]
|
104
|
+
end
|
105
|
+
|
106
|
+
def outputs # rubocop:disable Metrics/MethodLength
|
107
|
+
[
|
108
|
+
'if [ "$severity5" = "null" ]; then ' \
|
109
|
+
'echo "ERROR: Wrong ImageID or problem during vulnerabilities count." >&2; ' \
|
110
|
+
'exit 1; fi',
|
111
|
+
'if [ "$severity4" = "null" ]; then ' \
|
112
|
+
'echo "ERROR: Wrong ImageID or problem during vulnerabilities count." >&2; ' \
|
113
|
+
'exit 1; fi',
|
114
|
+
'echo "Severity5: $severity5, Severity4: $severity4"',
|
115
|
+
'risk=$((($severity5*3)+($severity4)))',
|
116
|
+
'echo "Risk: $risk"',
|
117
|
+
"if (($risk > \"#{opts[:acceptable_risk]}\")); then " \
|
118
|
+
'echo "Too many vulnerabilities. Severity5: $severity5, Severity4: $severity4" >&2; ' \
|
119
|
+
'exit 1; fi'
|
120
|
+
]
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
class Job
|
5
|
+
class Skopeo
|
6
|
+
# Pipedawg::Job::Skopeo::Copy class
|
7
|
+
class Copy < Job::Skopeo
|
8
|
+
def initialize(name, opts = {})
|
9
|
+
opts = {
|
10
|
+
config: {}, copy_image: name, destinations: [{ copy_image: nil, flags: [], options: {} }], flags: [],
|
11
|
+
logins: {}, options: {}, stage: '${CI_PROJECT_DIR}/stage', trusted_ca_cert_source_files: [],
|
12
|
+
trusted_ca_cert_target_file: '/etc/docker/certs.d/ca.crt'
|
13
|
+
}.merge(opts)
|
14
|
+
super name, opts
|
15
|
+
update
|
16
|
+
end
|
17
|
+
|
18
|
+
def update # rubocop:disable Metrics/AbcSize
|
19
|
+
require 'json'
|
20
|
+
opts[:script] = debug + config + cert_copies + login + mkstage + pull + (
|
21
|
+
opts[:destinations].map { |d| push(d) }
|
22
|
+
).flatten(1)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def config
|
28
|
+
['export CONFIG=$(mktemp -d)', "echo #{opts[:config].to_json.inspect} > \"${CONFIG}/config.json\""]
|
29
|
+
end
|
30
|
+
|
31
|
+
def cert_copies
|
32
|
+
["mkdir -p $(dirname \"#{opts[:trusted_ca_cert_target_file]}\")"] +
|
33
|
+
Array(opts[:trusted_ca_cert_source_files]).map do |cert|
|
34
|
+
"cat \"#{cert}\" >> \"#{opts[:trusted_ca_cert_target_file]}\""
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def login
|
39
|
+
opts.fetch(:logins, {}).map do |k, v|
|
40
|
+
begin
|
41
|
+
command = "echo \"#{v['password']}\" | #{opts[:command]} login --authfile \"${CONFIG}/config.json\" --username \"#{v['username']}\" --password-stdin \"#{k}\"" # rubocop:disable Layout/LineLength
|
42
|
+
`#{command}`
|
43
|
+
puts "Login succeeded for #{k}"
|
44
|
+
rescue RuntimeError => e
|
45
|
+
puts "Login failed for #{k}: #{e.message}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def mkstage
|
51
|
+
["mkdir -p \"#{opts[:stage]}\""]
|
52
|
+
end
|
53
|
+
|
54
|
+
def pull
|
55
|
+
copy(opts, "docker://#{opts[:copy_image]}", "\"dir://#{opts[:stage]}\"")
|
56
|
+
end
|
57
|
+
|
58
|
+
def push(destination_opts)
|
59
|
+
copy(destination_opts, "\"dir://#{opts[:stage]}\"", "docker://#{destination_opts[:copy_image]}")
|
60
|
+
end
|
61
|
+
|
62
|
+
def copy(copy_opts, source, destination)
|
63
|
+
Array(["#{opts[:command]} copy --authfile \"${CONFIG}/config.json\"", flags(copy_opts), options(copy_opts),
|
64
|
+
source, destination].reject(&:empty?).join(' '))
|
65
|
+
end
|
66
|
+
|
67
|
+
def flags(opts)
|
68
|
+
opts.fetch(:flags, []).uniq.map { |f| "--#{f}" }.join(' ')
|
69
|
+
end
|
70
|
+
|
71
|
+
def options(opts)
|
72
|
+
opts.fetch(:options, {}).map { |k, v| "--#{k} \"#{v}\"" }.join(' ')
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
class Job
|
5
|
+
# Pipedawg::Job::Skopeo class
|
6
|
+
class Skopeo < Job
|
7
|
+
def initialize(name, opts = {})
|
8
|
+
opts = {
|
9
|
+
command: 'skopeo',
|
10
|
+
image: { entrypoint: [''], name: 'quay.io/skopeo/stable:latest' }
|
11
|
+
}.merge(opts)
|
12
|
+
super name, opts
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/pipedawg/job.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
# job class
|
5
|
+
class Job
|
6
|
+
attr_accessor :name, :opts
|
7
|
+
|
8
|
+
def initialize(name = 'build', opts = {}) # rubocop:disable Metrics/MethodLength
|
9
|
+
@name = name
|
10
|
+
@opts = {
|
11
|
+
artifacts: {},
|
12
|
+
cache: {},
|
13
|
+
debug: true,
|
14
|
+
image: { name: 'ruby:2.5' },
|
15
|
+
needs: [],
|
16
|
+
retry: nil,
|
17
|
+
rules: nil,
|
18
|
+
script: [],
|
19
|
+
stage: 'build',
|
20
|
+
tags: [],
|
21
|
+
variables: nil
|
22
|
+
}.merge(opts)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hash
|
26
|
+
keys = %i[artifacts cache image needs retry rules script stage tags variables]
|
27
|
+
{ "#{name}": opts.slice(*keys).compact }
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def debug
|
33
|
+
if opts[:debug]
|
34
|
+
Pipedawg::Util.echo_proxy_vars
|
35
|
+
else
|
36
|
+
[]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
# pipeline class
|
5
|
+
class Pipeline
|
6
|
+
attr_accessor :name, :opts
|
7
|
+
|
8
|
+
def initialize(name = 'pipeline', opts = {})
|
9
|
+
@name = name
|
10
|
+
@opts = {
|
11
|
+
jobs: [Pipedawg::Job.new],
|
12
|
+
stages: ['build'],
|
13
|
+
workflow: nil
|
14
|
+
}.merge(opts)
|
15
|
+
update
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_yaml
|
19
|
+
require 'json'
|
20
|
+
require 'yaml'
|
21
|
+
pipeline = opts.compact.reject { |k, _| %i[jobs].include? k }
|
22
|
+
opts[:jobs].each do |job|
|
23
|
+
pipeline.merge!(job.to_hash)
|
24
|
+
end
|
25
|
+
JSON.parse(pipeline.to_json).to_yaml
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_yaml_file(file = 'pipeline.yml')
|
29
|
+
File.write(file, to_yaml)
|
30
|
+
end
|
31
|
+
|
32
|
+
def update
|
33
|
+
stages = []
|
34
|
+
opts[:jobs].each do |job|
|
35
|
+
stage = stage_from_needs(opts[:jobs], job.name)
|
36
|
+
stages << stage
|
37
|
+
job.opts[:stage] = stage.to_s
|
38
|
+
end
|
39
|
+
opts[:stages] = stages.uniq.sort.map(&:to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def stage_from_needs(jobs, job_name)
|
45
|
+
if jobs.select { |job| job.name == job_name }[0].opts.fetch(:needs, []) == []
|
46
|
+
1
|
47
|
+
else
|
48
|
+
jobs.select { |job| job.name == job_name }[0].opts[:needs].map { |need| stage_from_needs(jobs, need) }.max + 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pipedawg
|
4
|
+
# util class
|
5
|
+
class Util
|
6
|
+
def self.expand_env_vars(item) # rubocop:disable Metrics/MethodLength
|
7
|
+
case item
|
8
|
+
when Array
|
9
|
+
item.map { |i| expand_env_vars(i) }
|
10
|
+
when Hash
|
11
|
+
item.each { |k, v| item[k] = expand_env_vars(v) }
|
12
|
+
item.transform_keys! { |k| expand_env_vars(k) }
|
13
|
+
item
|
14
|
+
when String
|
15
|
+
item.gsub(/\${([^} ]+)}/) do |e|
|
16
|
+
ENV[e.gsub('${', '').gsub('}', '')]
|
17
|
+
end
|
18
|
+
else
|
19
|
+
item
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.puts_proxy_vars
|
24
|
+
puts 'Proxy settings:'
|
25
|
+
puts "http_proxy: #{ENV['http_proxy']}"
|
26
|
+
puts "https_proxy: #{ENV['https_proxy']}"
|
27
|
+
puts "no_proxy: #{ENV['no_proxy']}"
|
28
|
+
puts "HTTP_PROXY: #{ENV['HTTP_PROXY']}"
|
29
|
+
puts "HTTPS_PROXY: #{ENV['HTTPS_PROXY']}"
|
30
|
+
puts "NO_PROXY: #{ENV['NO_PROXY']}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.echo_proxy_vars
|
34
|
+
script = ['echo Proxy settings:']
|
35
|
+
script << 'echo http_proxy: "${http_proxy}"'
|
36
|
+
script << 'echo https_proxy: "${https_proxy}"'
|
37
|
+
script << 'echo no_proxy: "${no_proxy}"'
|
38
|
+
script << 'echo HTTP_PROXY: "${HTTP_PROXY}"'
|
39
|
+
script << 'echo HTTPS_PROXY: "${HTTPS_PROXY}"'
|
40
|
+
script << 'echo NO_PROXY: "${NO_PROXY}"'
|
41
|
+
script
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/pipedawg.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pipedawg/job'
|
4
|
+
require 'pipedawg/job/helm'
|
5
|
+
require 'pipedawg/job/helm/copy'
|
6
|
+
require 'pipedawg/job/kaniko'
|
7
|
+
require 'pipedawg/job/kaniko/build'
|
8
|
+
require 'pipedawg/job/qualys'
|
9
|
+
require 'pipedawg/job/qualys/scan'
|
10
|
+
require 'pipedawg/job/skopeo'
|
11
|
+
require 'pipedawg/job/skopeo/copy'
|
12
|
+
require 'pipedawg/pipeline'
|
13
|
+
require 'pipedawg/util'
|
14
|
+
require 'pipedawg/version'
|
15
|
+
|
16
|
+
module Pipedawg
|
17
|
+
class Error < StandardError; end
|
18
|
+
# Your code goes here...
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pipedawg-vl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- harbottle
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-05-16 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Generate GitLab CI pipelines.
|
14
|
+
email:
|
15
|
+
- harbottle@room3d3.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- LICENSE.txt
|
21
|
+
- README.md
|
22
|
+
- lib/pipedawg.rb
|
23
|
+
- lib/pipedawg/job.rb
|
24
|
+
- lib/pipedawg/job/helm.rb
|
25
|
+
- lib/pipedawg/job/helm/copy.rb
|
26
|
+
- lib/pipedawg/job/kaniko.rb
|
27
|
+
- lib/pipedawg/job/kaniko/build.rb
|
28
|
+
- lib/pipedawg/job/qualys.rb
|
29
|
+
- lib/pipedawg/job/qualys/scan.rb
|
30
|
+
- lib/pipedawg/job/skopeo.rb
|
31
|
+
- lib/pipedawg/job/skopeo/copy.rb
|
32
|
+
- lib/pipedawg/pipeline.rb
|
33
|
+
- lib/pipedawg/util.rb
|
34
|
+
- lib/pipedawg/version.rb
|
35
|
+
homepage: https://github.com/ValdrinLushaj/pipedawg
|
36
|
+
licenses:
|
37
|
+
- MIT
|
38
|
+
metadata:
|
39
|
+
homepage_uri: https://github.com/ValdrinLushaj/pipedawg
|
40
|
+
source_code_uri: https://github.com/ValdrinLushaj/pipedawg
|
41
|
+
changelog_uri: https://github.com/ValdrinLushaj/pipedawg
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubygems_version: 3.4.10
|
58
|
+
signing_key:
|
59
|
+
specification_version: 4
|
60
|
+
summary: Generate GitLab CI pipelines.
|
61
|
+
test_files: []
|