From f9b8e22d63018f2197b46af018f4eb141a3f8577 Mon Sep 17 00:00:00 2001 From: John Viega Date: Thu, 19 Oct 2023 15:46:50 -0400 Subject: [PATCH] Jtv/releaseitems (#67) * Use explicit targets for module imports * Removing the default config store; for current semantics the CWD default seems better than assuming a URL. * Did a lint pass; dealt with some unused variables, including turning off the 'hint' messages for stuff we want to keep around * Add in some of the new configs. * A tiny bit of polishing * tweak * Some beautification * Initial untested param work done * fixing missing quote from the wrap_entrypoints.c4m * ionly downloading chalk binaries when doing docker build * bumping con4m which fixes is_file path resolution * Have the arch binary updating happen any time an update happens. * Add dumping for cached config modules too * Get rid of old configs * Incorporate new versions of con4m / nimu with ability to get an external IP associated w/ the host portably. Used it in the reporting server to make it not be hacky, and added PUBLIC_IPV4_ADDR_WHEN_CHALKED and _OP_PUBLIC_IPV4_ADDR. * Clean up a bit * Update release notes; integrate Mark's change in tag-line * Update autocomplete. * Fixed circular imports * Remove the chalk report from `chalk load`. It isn't needed and is too much for good intros to the product. * bump nimu * fix(docgen): add con4m defintions to fix docgen cmd regression * docs(typos): fixing various typos found via vale.sh (#46) * fix(docker): wrap_entrypoint honors command args As chalk exec would parse the full command, if it had any arguments chalk was not familiar with, chalk would not parse it correctly and therefore will call command incorrectly. By adding -- after the command name, chalk ignores rest of the args and passes them as-is to the command. For example this now works: ``` ENTRYPOINT ["ls", "-la"] ``` As chalk will end up calling it as: ``` /chalk exec --exec-command-name=ls -- -la ``` * Updated two of the howtos * reverting dockerfile change * Re-add in the ability to dump to a file * fix(docs): fix doc/link regressions * fix(docs): link fix * fix(docs): more reformatting / regression fixing * fix(docs): shockingly, more formatting fixes * fix(docs): reverting the multiple regression fixes because there wasn't a regression * Don't let docgen output static docs * fix(docs) formatting * fix(README): fix link to docker howto * ansi fix --------- Co-authored-by: Miroslav Shubernetskiy Co-authored-by: Rich Smith --- README.md | 6 +- chalk.nimble | 6 +- configs/app-inventory.c4m | 32 --- configs/app_inventory.c4m | 3 + configs/basic_compliance.c4m | 2 + configs/compliance-other.c4m | 9 - configs/compliance_docker.c4m | 3 + ...{compliance-docker.c4m => embed_sboms.c4m} | 6 +- configs/guide-heartbeat.c4m | 53 ---- configs/{new => }/impersonate_docker.c4m | 0 configs/{new => }/log_report.c4m | 0 configs/net-heartbeat.c4m | 50 ---- configs/new/compliance_docker.c4m | 3 - configs/new/wrap_entrypoints.c4m | 2 - configs/{new => }/reporting_server.c4m | 16 +- configs/{new => }/terminal_report.c4m | 0 configs/use_heartbeats.c4m | 29 ++ configs/wrap_entrypoints.c4m | 30 ++ src/api.nim | 15 +- src/autocomplete/default.bash | 9 +- src/autocomplete/mac.bash | 7 +- src/chalk.nim | 6 +- src/chalk_common.nim | 10 +- src/chalkjson.nim | 10 +- src/commands/cmd_docker.nim | 39 ++- src/commands/cmd_dump.nim | 68 ++++- src/commands/cmd_help.nim | 12 +- src/commands/cmd_load.nim | 2 - src/commands/cmd_logout.nim | 8 +- src/commands/cmd_version.nim | 12 +- src/configs/README.md | 7 +- src/configs/base_chalk_templates.c4m | 2 + src/configs/base_init.c4m | 1 - src/configs/base_keyspecs.c4m | 33 ++- src/configs/base_plugins.c4m | 12 +- src/configs/base_report_templates.c4m | 9 + src/configs/chalk.c42spec | 52 +++- src/configs/getopts.c4m | 182 +++++++++--- src/configs/ioconfig.c4m | 5 - src/confload.nim | 10 +- src/docs/core-hashing.md | 6 +- src/docs/core-release-notes.md | 65 ++++- src/docs/core-secret-manager-api.md | 10 +- src/docs/guide-config-overview.md | 4 +- src/docs/guide-getting-started.md | 12 +- src/docs/guide-heartbeat.md | 2 +- src/docs/guide-user-guide.md | 42 +-- src/docs/howto-app-inventory.md | 260 ++++++------------ src/docs/howto-compliance.md | 118 ++++---- ...owto-deploy-chalk-globally-using-docker.md | 92 +++++++ src/docs/howto-net-services.md | 4 +- src/docs/img/appinv-ss1.png | Bin 0 -> 66233 bytes src/plugin_api.nim | 24 -- src/plugins/elf.nim | 4 + src/plugins/procfs.nim | 11 +- src/plugins/system.nim | 1 + src/selfextract.nim | 156 +++++++++-- src/util.nim | 5 +- 58 files changed, 942 insertions(+), 635 deletions(-) delete mode 100644 configs/app-inventory.c4m create mode 100644 configs/app_inventory.c4m create mode 100644 configs/basic_compliance.c4m delete mode 100644 configs/compliance-other.c4m create mode 100644 configs/compliance_docker.c4m rename configs/{compliance-docker.c4m => embed_sboms.c4m} (62%) delete mode 100644 configs/guide-heartbeat.c4m rename configs/{new => }/impersonate_docker.c4m (100%) rename configs/{new => }/log_report.c4m (100%) delete mode 100644 configs/net-heartbeat.c4m delete mode 100644 configs/new/compliance_docker.c4m delete mode 100644 configs/new/wrap_entrypoints.c4m rename configs/{new => }/reporting_server.c4m (71%) rename configs/{new => }/terminal_report.c4m (100%) create mode 100644 configs/use_heartbeats.c4m create mode 100644 configs/wrap_entrypoints.c4m create mode 100644 src/docs/howto-deploy-chalk-globally-using-docker.md create mode 100644 src/docs/img/appinv-ss1.png diff --git a/README.md b/README.md index 473cb881..75722e6d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![tests](https://github.com/crashappsec/chalk/actions/workflows/tests.yml/badge.svg?branch=main&event=push)](https://github.com/crashappsec/chalk/actions/workflows/tests.yml?query=branch%3Amain) -# Total visibility of your software engineering lifecycle. +# Telemetry & observability for the software development lifecycle. ## About Chalk @@ -32,6 +32,10 @@ Understanding which services run in containers can help you build a service map. Automatically create SBOMs for every build of every code repo, including auto-deploying and using built-in SBOM generation and collection tools. Send these SBOMs to a central location for further analysis, and to maintain a record across your environment. Follow this how-to on our docs site [here](https://crashoverride.com/docs/how-to-guides/how-to-create-and-maintain-an-sbom-registry). +#### How-to deploy Chalk globally using Docker + +You can deploy Chalk by setting a global alias for Docker and having it call Chalk, so that every build that runs through your build server using Docker, will automatically be 'chalked'. It's a technique that can be combined with chalks ability to deploy tools and configure monitoring, to automatically add security controls and collect information for every application. Follow this how-to on our docs site [here](https://crashoverride.com/docs/how-to-guides/how-to-deploy-chalk-globally-using-docker)] + All documentation for Chalk is available at https://crashoverride.com/docs and is also fully accessible though the command line interface. ## Getting started diff --git a/chalk.nimble b/chalk.nimble index efa1d4cb..f1bf918e 100644 --- a/chalk.nimble +++ b/chalk.nimble @@ -1,4 +1,4 @@ -version = "0.1.2" +version = "0.1.3" author = "John Viega" description = "Software artifact metadata to make it easy to tie " & "deployments to source code and collect metadata." @@ -8,7 +8,7 @@ bin = @["chalk"] # Dependencies requires "nim >= 2.0.0" -requires "https://github.com/crashappsec/con4m#da5a430616ef2740da603438b35436d184c36938" +requires "https://github.com/crashappsec/con4m#816585633835c30e5aaf4f53fdfb8eb4dd91f97a" requires "https://github.com/viega/zippy == 0.10.7" requires "https://github.com/aruZeta/QRgen == 3.0.0" @@ -70,7 +70,7 @@ before install: after build: when not defined(debug): exec "set -x && strip " & bin[0] - exec "set -x && ./" & bin[0] & " --no-use-external-config --skip-command-report load default" + exec "set -x && ./" & bin[0] & " --debug --no-use-external-config --skip-command-report load default" task debug, "Get a debug build": # additional flags are configured in config.nims diff --git a/configs/app-inventory.c4m b/configs/app-inventory.c4m deleted file mode 100644 index 31e56b48..00000000 --- a/configs/app-inventory.c4m +++ /dev/null @@ -1,32 +0,0 @@ -# Make it easy to impersonate docker. -default_command: "docker" - -# Collect runtime information by wrapping docker entry points. -docker.wrap_entrypoint: true - -# Set up output to our API server. -# This bit configures the info. - -sink_config output_to_http { - enabled: true - sink: "post" - uri: "http://localhost:8585/report" -} - - -# We subscribe our new sink to the "report" topic, which is the main -# report for any command. - -subscribe("report", "output_to_http") - -# Unsubscribe the 'default' log file report: - -unsubscribe("report", "default_out") - - -# Optional: remove the terminal summary reports, which -# are additional `custom` reports. These just need to have -# their `enabled` property turned off. - -# custom_report.terminal_chalk_time.enabled: false -# custom_report.terminal_other_op.enabled: false diff --git a/configs/app_inventory.c4m b/configs/app_inventory.c4m new file mode 100644 index 00000000..d0640dbc --- /dev/null +++ b/configs/app_inventory.c4m @@ -0,0 +1,3 @@ +use reporting_server from "https://chalkdust.io" +use wrap_entrypoints from "https://chalkdust.io" +use impersonate_docker from "https://chalkdust.io" diff --git a/configs/basic_compliance.c4m b/configs/basic_compliance.c4m new file mode 100644 index 00000000..6c302c5a --- /dev/null +++ b/configs/basic_compliance.c4m @@ -0,0 +1,2 @@ +use reporting_server from "https://chalkdust.io" +run_sbom_tools: true diff --git a/configs/compliance-other.c4m b/configs/compliance-other.c4m deleted file mode 100644 index 20b5cb56..00000000 --- a/configs/compliance-other.c4m +++ /dev/null @@ -1,9 +0,0 @@ -run_sbom_tools: true - -# `chalk insert` uses the mark_default template. -mark_template.mark_default.key.SBOM.use: true - -# `chalk docker build` uses the `minimal` template. -mark_template.minimal.key.SBOM.use: true - -default_command: "insert" \ No newline at end of file diff --git a/configs/compliance_docker.c4m b/configs/compliance_docker.c4m new file mode 100644 index 00000000..edd5cb21 --- /dev/null +++ b/configs/compliance_docker.c4m @@ -0,0 +1,3 @@ +use impersonate_docker from "https://chalkdust.io" +use basic_compliance from "https://chalkdust.io" + diff --git a/configs/compliance-docker.c4m b/configs/embed_sboms.c4m similarity index 62% rename from configs/compliance-docker.c4m rename to configs/embed_sboms.c4m index 3f5b3a46..91dc3d99 100644 --- a/configs/compliance-docker.c4m +++ b/configs/embed_sboms.c4m @@ -1,9 +1,7 @@ -run_sbom_tools: true +# Embed sboms in chalk marks. # `chalk insert` uses the mark_default template. mark_template.mark_default.key.SBOM.use: true # `chalk docker build` uses the `minimal` template. -mark_template.minimal.key.SBOM.use: true - -default_command: "docker" \ No newline at end of file +mark_template.minimal.key.SBOM.use: true \ No newline at end of file diff --git a/configs/guide-heartbeat.c4m b/configs/guide-heartbeat.c4m deleted file mode 100644 index 96fa213c..00000000 --- a/configs/guide-heartbeat.c4m +++ /dev/null @@ -1,53 +0,0 @@ -# exec heartbeat -exec.heartbeat: true -exec.heartbeat_rate: <<10 seconds>> - -# docker wrapping -docker.wrap_entrypoint: true - -# network reporting template -report_template network_report { - key.CHALK_ID.use = true - key.CHALK_PTR.use = true - - key._OPERATION.use = true - key._TIMESTAMP.use = true - - key._CHALKS.use = true - - key._OP_PLATFORM.use = true - key._OP_HOSTNAME.use = true - key._OP_HOSTINFO.use = true - key._OP_NODENAME.use = true - - key._OP_ERRORS.use = true - - key._OP_TCP_SOCKET_INFO.use = true - key._OP_UDP_SOCKET_INFO.use = true - key._OP_IPV4_ROUTES.use = true - key._OP_IPV6_ROUTES.use = true - key._OP_IPV4_INTERFACES.use = true - key._OP_IPV6_INTERFACES.use = true - key._OP_ARP_TABLE.use = true -} - - -# output sinks -sink_config network_std_out { - sink: "stdout" - enabled: true -} - -sink_config network_file_out { - sink: "file" - enabled: true - filename: "~/network_heartbeat_log.log" -} - -# custom reporting -custom_report network_heartbeat_report { - enabled: true - report_template: "network_report" - sink_configs: ["network_std_out", "network_file_out"] - use_when: ["heartbeat"] -} diff --git a/configs/new/impersonate_docker.c4m b/configs/impersonate_docker.c4m similarity index 100% rename from configs/new/impersonate_docker.c4m rename to configs/impersonate_docker.c4m diff --git a/configs/new/log_report.c4m b/configs/log_report.c4m similarity index 100% rename from configs/new/log_report.c4m rename to configs/log_report.c4m diff --git a/configs/net-heartbeat.c4m b/configs/net-heartbeat.c4m deleted file mode 100644 index 061731ee..00000000 --- a/configs/net-heartbeat.c4m +++ /dev/null @@ -1,50 +0,0 @@ -exec.heartbeat: true -exec.heartbeat_rate: <<30 minutes>> - -sink_config output_to_screen { - sink: "stdout" - enabled: true -} - -sink_config output_to_http { - enabled: false - sink: "post" - uri: "http://some.web.location/webhook" -} - -sink_config output_to_file { - sink: "file" - enabled: false - filename: "/tmp/network_heartbeat.log" -} - -custom_report network_heartbeat_report { - report_template: "network_report" - # you can add/remove sinks in the list defined below - sink_configs: ["output_to_screen", "output_to_file", "output_to_http"] - use_when: ["heartbeat"] -} - -# docker wrapping -docker.wrap_entrypoint: true - -# network reporting template -report_template network_report { - key.CHALK_ID.use = true - key.CHALK_PTR.use = true - key._OPERATION.use = true - key._TIMESTAMP.use = true - key._CHALKS.use = true - key._OP_PLATFORM.use = true - key._OP_HOSTNAME.use = true - key._OP_HOSTINFO.use = true - key._OP_NODENAME.use = true - key._OP_ERRORS.use = true - key._OP_TCP_SOCKET_INFO.use = true - key._OP_UDP_SOCKET_INFO.use = true - key._OP_IPV4_ROUTES.use = true - key._OP_IPV6_ROUTES.use = true - key._OP_IPV4_INTERFACES.use = true - key._OP_IPV6_INTERFACES.use = true - key._OP_ARP_TABLE.use = true -} diff --git a/configs/new/compliance_docker.c4m b/configs/new/compliance_docker.c4m deleted file mode 100644 index 855d1298..00000000 --- a/configs/new/compliance_docker.c4m +++ /dev/null @@ -1,3 +0,0 @@ -use impersonate_docker -use reporting_server -use wrap_entrypoints diff --git a/configs/new/wrap_entrypoints.c4m b/configs/new/wrap_entrypoints.c4m deleted file mode 100644 index 21d7cef5..00000000 --- a/configs/new/wrap_entrypoints.c4m +++ /dev/null @@ -1,2 +0,0 @@ -# Ensures entrypoint wrapping is enabled in the config" -docker.wrap_entrypoint: true diff --git a/configs/new/reporting_server.c4m b/configs/reporting_server.c4m similarity index 71% rename from configs/new/reporting_server.c4m rename to configs/reporting_server.c4m index 54e13440..e26212b2 100644 --- a/configs/new/reporting_server.c4m +++ b/configs/reporting_server.c4m @@ -1,5 +1,3 @@ -# TODO: default - func validate_url(url) { result := "" @@ -9,13 +7,7 @@ func validate_url(url) { } func get_local_url() { - out, code := system("ifconfig -a | grep inet | grep broadcast | head -1 | " + - "awk '{ print $2 }'") - if code != 0 { - return "https://localhost:7890" - } - - return "https://" + out.strip() + ":7890" + return "https://" + external_ip() + ":7890" } parameter sink_config.output_to_http.uri { @@ -23,6 +15,12 @@ parameter sink_config.output_to_http.uri { doc: """ A config for sending reports to a custom implementation of the test reporting server. + +Run the server via: + +``` +docker run --rm - -w /db -v $HOME/.local/c0/:/db -p 8585:8585 ghcr.io/crashappsec/chalk-test-server +``` """ validator: func validate_url(string) -> string default: func get_local_url() -> string diff --git a/configs/new/terminal_report.c4m b/configs/terminal_report.c4m similarity index 100% rename from configs/new/terminal_report.c4m rename to configs/terminal_report.c4m diff --git a/configs/use_heartbeats.c4m b/configs/use_heartbeats.c4m new file mode 100644 index 00000000..0f739c47 --- /dev/null +++ b/configs/use_heartbeats.c4m @@ -0,0 +1,29 @@ +# We use a con4m duration field for the actual `heartbeat_rate` field, +# But I don't want people to have to worry about that. + +func validate_heartbeat_freq(f: float) { + if (f <= 0.0) { + return "Value must be greater than 0" + } else { + return "" + } +} + +parameter var heartbeat_minute_frequency { + default: 30.0 + validator: func validate_heartbeat_freq(float) -> string + shortdoc: "Heartbeat Frequency (minutes)" + doc: """ +This value sets how many minutes to wait between heartbeats. Fractions +of a minute are okay. +""" +} +var heartbeat_minute_frequency: float + +minutes := int(heartbeat_minute_frequency) +sec_as_f := (heartbeat_minute_frequency - float(minutes)) * 60.0 +sec := int(sec_as_f) + +duration := Duration($(minutes) + " min " + $(sec) + " sec") +exec.heartbeat: true +exec.heartbeat_rate: duration diff --git a/configs/wrap_entrypoints.c4m b/configs/wrap_entrypoints.c4m new file mode 100644 index 00000000..e84bb3aa --- /dev/null +++ b/configs/wrap_entrypoints.c4m @@ -0,0 +1,30 @@ +# Ensures entrypoint wrapping is enabled in the config" +docker.wrap_entrypoint: true + +myarch := arch() +binary_dir := "~/.local/chalk/bin/linux-" + myarch + "/" + +if osname() == "macosx" { + if not is_dir(binary_dir) { + mkdir(binary_dir) + } + + linux_chalk_location := binary_dir + "chalk" + docker.arch_binary_locations = { "linux/" + myarch : linux_chalk_location } + + if not is_file(linux_chalk_location) { + echo("MacOS requires downloading a Linux binary to wrap " + + "docker entry points.") + chalk_url_base := "https://crashoverride.com/dl/chalk/chalk-" + chalk_url := chalk_url_base + version() + "-linux-" + myarch + + info("Downloading chalk from: " + chalk_url) + + bits := url_get(chalk_url) + + info("Writing to: " + linux_chalk_location) + write_file(linux_chalk_location, bits) + config := run(program_path() + " dump") + write_file(binary_dir + "config.c4m", config) + } +} diff --git a/src/api.nim b/src/api.nim index ada7e99a..665e7015 100644 --- a/src/api.nim +++ b/src/api.nim @@ -12,19 +12,18 @@ template jwtSplitAndDecode(jwtString: string, doDecode: bool): string = let parts = split(jwtString, '.') if len(parts) != 3: raise newException(Exception, "Invalid JWT format") - let apiJwtPayload = parts[1] if doDecode: let decodedApiJwt = decode(apiJwtPayload) $decodedApiJwt else: - $apiJwtPayload + $(parts[1]) #apiJwtPayload proc refreshAccessToken*(refresh_token: string): string = # Mechanism to support access_token refresh via OIDC let timeout: int = cast[int](chalkConfig.getSecretManagerTimeout()) - var + var refresh_url = uri.parseUri(chalkConfig.getSecretManagerUrl()) context: SslContext client: HttpClient @@ -34,7 +33,7 @@ proc refreshAccessToken*(refresh_token: string): string = # request new access_token via refresh info("Refreshing API access token....") if refresh_url.scheme == "https": - let context = newContext(verifyMode = CVerifyPeer) + context = newContext(verifyMode = CVerifyPeer) client = newHttpClient(sslContext = context, timeout = timeout) else: client = newHttpClient(timeout = timeout) @@ -43,9 +42,9 @@ proc refreshAccessToken*(refresh_token: string): string = if response.status.startswith("200"): # parse json response and save / return values - let jsonNode = parseJson(response.body()) - let new_access_token = jsonNode["access_token"].getStr() - let new_id_token = jsonNode["id_token"].getStr() + let + jsonNode = parseJson(response.body()) + new_access_token = jsonNode["access_token"].getStr() return new_access_token @@ -67,7 +66,6 @@ proc getChalkApiToken*(): (string, string) = contextPoll: SslContext frameIndex: int = 0 framerate: float - jwtString: string pollPayloadBase64: string pollUri: Uri pollUrl: string @@ -155,7 +153,6 @@ proc getChalkApiToken*(): (string, string) = # decode JWT pollPayloadBase64 = jwtSplitAndDecode($accessToken, true) - let decodedPollJwt = parseJson(pollPayloadBase64) ret = ($accessToken, $refreshToken) elif responsePoll.status.startswith("428") or responsePoll.status.startswith("403"): diff --git a/src/autocomplete/default.bash b/src/autocomplete/default.bash index 660be47b..6395205e 100644 --- a/src/autocomplete/default.bash +++ b/src/autocomplete/default.bash @@ -39,7 +39,7 @@ function _chalk_delete_completions { function _chalk_load_completions { if [ ${_CHALK_CUR_WORD::1} = "-" ] ; then - COMPREPLY=($(compgen -W "--color --no-color --help --log-level --config-file --enable-report --disable-report --report-cache-file --time --no-time --use-embedded-config --no-use-embedded-config --use-external-config --no-use-external-config --show-config --no-show-config --use-report-cache --no-use-report-cache --debug --no-debug --replace --no-replace --validation --no-validation --validation-warning --no-validation-warning" -- ${_CHALK_CUR_WORD})) + COMPREPLY=($(compgen -W "--color --no-color --help --log-level --config-file --enable-report --disable-report --report-cache-file --time --no-time --use-embedded-config --no-use-embedded-config --use-external-config --no-use-external-config --show-config --no-show-config --use-report-cache --no-use-report-cache --debug --no-debug --replace --no-replace --update-arch-binaries --no-update-arch-binaries --params --no-params --validation --no-validation --validation-warning --no-validation-warning" -- ${_CHALK_CUR_WORD})) fi if [[ $_CHALK_CUR_IX -le $COMP_CWORD ]] ; then @@ -55,7 +55,10 @@ function _chalk_load_completions { function _chalk_dump_completions { if [ ${_CHALK_CUR_WORD::1} = "-" ] ; then - COMPREPLY=($(compgen -W "--color --no-color --help --log-level --config-file --enable-report --disable-report --report-cache-file --time --no-time --use-embedded-config --no-use-embedded-config --use-external-config --no-use-external-config --show-config --no-show-config --use-report-cache --no-use-report-cache --debug --no-debug --validation --no-validation --validation-warning --no-validation-warning" -- ${_CHALK_CUR_WORD})) + COMPREPLY=($(compgen -W "--color --no-color --help --log-level --config-file --enable-report --disable-report --report-cache-file --time --no-time --use-embedded-config --no-use-embedded-config --use-external-config --no-use-external-config --show-config --no-show-config --use-report-cache --no-use-report-cache --debug --no-debug --validation --no-validation --validation-warning --no-validation-warning"-- ${_CHALK_CUR_WORD})) + else + EXTRA=($(compgen -W "params cache" -- ${_CHALK_CUR_WORD})) + COMPREPLY+=(${EXTRA[@]}) fi if [[ $_CHALK_CUR_IX -le $COMP_CWORD ]] ; then @@ -183,4 +186,4 @@ function _chalk_completions { } complete -F _chalk_completions chalk -# { "MAGIC" : "dadfedabbadabbed", "CHALK_ID" : "6WRKEC-HPCM-S6AD-1GC5K3", "CHALK_VERSION" : "0.1.3", "TIMESTAMP_WHEN_CHALKED" : 1697490789233, "DATETIME_WHEN_CHALKED" : "2023-10-16T17:13:09.050-04:00", "ARTIFACT_TYPE" : "bash", "ARTIFACT_VERSION" : "0.1.3", "CHALK_PTR" : "This mark determines when to update the script. If there is no mark, or the mark is invalid it will be replaced. To customize w/o Chalk disturbing it when it can update, add a valid mark with a version key higher than the current chalk verison, or use version 0.0.0 to prevent updates", "HASH" : "71726e2e40af14b1833ce3023a07605fa9c83e2c8098c403950558de34a0f138", "INJECTOR_COMMIT_ID" : "db872eb11801a82587abced82074db7182b72c0a", "ORIGIN_URI" : "git@github.com:crashappsec/chalk.git", "METADATA_ID" : "SB7M3E-3C8G-AG1Z-28J767" } +# { "MAGIC" : "dadfedabbadabbed", "CHALK_ID" : "CRT3AS-HKCM-TK2E-1QC9JP", "CHALK_VERSION" : "0.1.3", "TIMESTAMP_WHEN_CHALKED" : 1697697525533, "DATETIME_WHEN_CHALKED" : "2023-10-19T02:38:42.027-04:00", "ARTIFACT_TYPE" : "bash", "ARTIFACT_VERSION" : "0.1.3", "CHALK_PTR" : "This mark determines when to update the script. If there is no mark, or the mark is invalid it will be replaced. To customize w/o Chalk disturbing it when it can update, add a valid mark with a version key higher than the current chalk verison, or use version 0.0.0 to prevent updates", "HASH" : "f45f3e5187becf34d5f654e94780022a5d41c15bcd03867b7a1c045ec431cf72", "INJECTOR_COMMIT_ID" : "8001314ff4406ee96ee75395320b3b806ab2f068", "ORIGIN_URI" : "git@github.com:crashappsec/chalk.git", "METADATA_ID" : "RBMBE0-C89J-2KPB-Z3JM78" } diff --git a/src/autocomplete/mac.bash b/src/autocomplete/mac.bash index 7213056b..39811ab8 100644 --- a/src/autocomplete/mac.bash +++ b/src/autocomplete/mac.bash @@ -32,7 +32,7 @@ function _chalk_delete_completions { function _chalk_load_completions { if [ ${_CHALK_CUR_WORD::1} = "-" ] ; then - COMPREPLY=($(compgen -W "--color --no-color --help --log-level --config-file --enable-report --disable-report --report-cache-file --time --no-time --use-embedded-config --no-use-embedded-config --use-external-config --no-use-external-config --show-config --no-show-config --use-report-cache --no-use-report-cache --debug --no-debug --replace --no-replace --validation --no-validation --validation-warning --no-validation-warning" -- ${_CHALK_CUR_WORD})) + COMPREPLY=($(compgen -W "--color --no-color --help --log-level --config-file --enable-report --disable-report --report-cache-file --time --no-time --use-embedded-config --no-use-embedded-config --use-external-config --no-use-external-config --show-config --no-show-config --use-report-cache --no-use-report-cache --debug --no-debug --replace --no-replace --update-arch-binaries --no-update-arch-binaries --params --no-params --validation --no-validation --validation-warning --no-validation-warning" -- ${_CHALK_CUR_WORD})) fi if [[ $_CHALK_CUR_IX -le $COMP_CWORD ]] ; then @@ -47,6 +47,9 @@ function _chalk_load_completions { function _chalk_dump_completions { if [ ${_CHALK_CUR_WORD::1} = "-" ] ; then COMPREPLY=($(compgen -W "--color --no-color --help --log-level --config-file --enable-report --disable-report --report-cache-file --time --no-time --use-embedded-config --no-use-embedded-config --use-external-config --no-use-external-config --show-config --no-show-config --use-report-cache --no-use-report-cache --debug --no-debug --validation --no-validation --validation-warning --no-validation-warning" -- ${_CHALK_CUR_WORD})) + else + EXTRA=($(compgen -W "params cache" -- ${_CHALK_CUR_WORD})) + COMPREPLY+=(${EXTRA[@]}) fi if [[ $_CHALK_CUR_IX -le $COMP_CWORD ]] ; then @@ -161,4 +164,4 @@ function _chalk_completions { } complete -F _chalk_completions chalk -# { "MAGIC" : "dadfedabbadabbed", "CHALK_ID" : "CMWKGR-V16G-RKCE-B5C9JK", "CHALK_VERSION" : "0.1.3", "TIMESTAMP_WHEN_CHALKED" : 1697490789234, "DATETIME_WHEN_CHALKED" : "2023-10-16T17:13:09.050-04:00", "ARTIFACT_TYPE" : "bash", "ARTIFACT_VERSION" : "0.1.3", "CHALK_PTR" : "This mark determines when to update the script. If there is no mark, or the mark is invalid it will be replaced. To customize w/o Chalk disturbing it when it can update, add a valid mark with a version key higher than the current chalk verison, or use version 0.0.0 to prevent updates", "HASH" : "e98ca4169ebe623b9686f253b71e135da5fa160d622a42dd8a6565906f7a5c52", "INJECTOR_COMMIT_ID" : "db872eb11801a82587abced82074db7182b72c0a", "ORIGIN_URI" : "git@github.com:crashappsec/chalk.git", "METADATA_ID" : "6RATQ8-XBKJ-S2BD-MFNK5P" } +# { "MAGIC" : "dadfedabbadabbed", "CHALK_ID" : "60W68D-31C8-S3JD-SQCSJK", "CHALK_VERSION" : "0.1.3", "TIMESTAMP_WHEN_CHALKED" : 1697697525538, "DATETIME_WHEN_CHALKED" : "2023-10-19T02:38:42.027-04:00", "ARTIFACT_TYPE" : "bash", "ARTIFACT_VERSION" : "0.1.3", "CHALK_PTR" : "This mark determines when to update the script. If there is no mark, or the mark is invalid it will be replaced. To customize w/o Chalk disturbing it when it can update, add a valid mark with a version key higher than the current chalk verison, or use version 0.0.0 to prevent updates", "HASH" : "08d4ab2977fe93a16d3f62c8ffe3492cbfa35d7f53cd574b8f7b17d99442a7d5", "INJECTOR_COMMIT_ID" : "8001314ff4406ee96ee75395320b3b806ab2f068", "ORIGIN_URI" : "git@github.com:crashappsec/chalk.git", "METADATA_ID" : "PHX481-971S-JMFX-4ZK4A3" } diff --git a/src/chalk.nim b/src/chalk.nim index 188e3110..403adc14 100644 --- a/src/chalk.nim +++ b/src/chalk.nim @@ -11,7 +11,7 @@ import config, confload, commands, norecurse, sinks, docker_base, attestation, util when isMainModule: - + setupSignalHandlers() # util.nim ioSetup() # sinks.nim loadAllConfigs() # confload.nim @@ -35,6 +35,8 @@ when isMainModule: of "delete": runCmdDelete(chalkConfig.getArtifactSearchPath()) of "env": runCmdEnv() of "dump": runCmdConfDump() + of "dump.params": runCmdConfDumpParams() + of "dump.cache": runCmdConfDumpCache() of "load": runCmdConfLoad() of "logout": runCmdLogout() of "login": runCmdLogin() @@ -48,6 +50,6 @@ when isMainModule: of "docgen": runChalkDocGen() # in cmd_help else: runChalkHelp(getCommandName()) # noreturn, will not show config. - + showConfigValues() quitChalk() diff --git a/src/chalk_common.nim b/src/chalk_common.nim index af795c58..c72a40bf 100644 --- a/src/chalk_common.nim +++ b/src/chalk_common.nim @@ -356,9 +356,13 @@ var template dumpExOnDebug*() = if chalkConfig != nil and chalkConfig.getChalkDebug(): - let msg = "Handling exception (msg = " & getCurrentExceptionMsg() & ")\n" & - getCurrentException().getStackTrace() - publish("debug", msg) + let + msg = "" # "Handling exception (msg = " & getCurrentExceptionMsg() & ")\n" + tb = "Traceback (most recent call last)\n" & + getCurrentException().getStackTrace() + ii = default(InstInfo) + + publish("debug", formatCompilerError(msg, nil, tb, ii)) proc getBaseCommandName*(): string = if '.' in commandName: diff --git a/src/chalkjson.nim b/src/chalkjson.nim index 9d987ca4..7be0ab59 100644 --- a/src/chalkjson.nim +++ b/src/chalkjson.nim @@ -38,7 +38,7 @@ const type ChalkJsonNode* = ref CJsonNodeObj CJSonError* = ref object of ValueError - CJsonNodeKind = enum + CJsonNodeKind* = enum CJNull, CJBool, CJInt, CJFloat, CJString, CJObject, CJArray CJsonNodeObj* {.acyclic.} = object @@ -51,7 +51,7 @@ type of CJObject: kvpairs*: OrderedTableRef[string, ChalkJsonNode] of CJArray: items*: seq[ChalkJsonNode] -proc chalkParseJson(s: Stream): ChalkJSonNode +proc chalkParseJson*(s: Stream): ChalkJSonNode proc findJsonStart*(stream: Stream): bool = ## Seeks the stream to the start of the JSON blob, when the stream @@ -149,7 +149,7 @@ proc valueFromJson(jobj: ChalkJsonNode, fname: string): Box = of CJObject: return pack(objFromJson(jobj, fname)) of CJArray: return pack(arrayFromJson(jobj, fname)) -proc jsonNodeToBox(n: ChalkJSonNode): Box = +proc jsonNodeToBox*(n: ChalkJSonNode): Box = case n.kind of CJNull: return nil of CJBool: return pack(n.boolval) @@ -419,7 +419,7 @@ proc jsonValue(s: Stream): ChalkJSonNode = else: raise parseError("Bad JSon at position: " & $(s.getPosition())) -proc chalkParseJson(s: Stream): ChalkJSonNode = +proc chalkParseJson*(s: Stream): ChalkJSonNode = s.jsonWS() result = s.jSonValue() # Per the spec, we should advance the stream white space after the @@ -503,7 +503,7 @@ proc getChalkMarkAsStr*(obj: ChalkObj): string = return obj.cachedMark trace("Converting Mark to JSON. Mark template is: " & getOutputConfig().markTemplate) - + if obj.cachedMark != "": return obj.cachedMark diff --git a/src/commands/cmd_docker.nim b/src/commands/cmd_docker.nim index 30af22d2..0e3eb384 100644 --- a/src/commands/cmd_docker.nim +++ b/src/commands/cmd_docker.nim @@ -295,6 +295,27 @@ proc findProperBinaryToCopyIntoContainer(ctx: DockerInvocation): string = ")") return "" +proc formatExecString(command: string): string = + let + parts = strutils.split(command, maxsplit = 1) + name = parts[0] + args = if len(parts) > 1: + " -- " & parts[1] + else: + "" + return strutils.strip("exec /chalk exec --exec-command-name " & + name & args) + +proc formatExecArray(args: JsonNode): string = + let arr = `%*`(["/chalk", "exec", "--exec-command-name"]) + var i = 0 + for item in args.items(): + if i == 1: + arr.add(`%`("--")) + arr.add(item) + i.inc() + return $(arr) + proc rewriteEntryPoint*(ctx: DockerInvocation) = var lastEntryPoint = EntryPointInfo(nil) @@ -341,14 +362,9 @@ proc rewriteEntryPoint*(ctx: DockerInvocation) = # will get ignored, as long as we keep ENTRYPOINT in string form. if lastEntryPoint.str != "": # In shell form, be a good citizen and exec so that `sh` isn't pid 1 - newInstruction = "ENTRYPOINT exec /chalk exec --exec-command-name " & - lastEntryPoint.str + newInstruction = "ENTRYPOINT " & formatExecString(lastEntryPoint.str) else: - let arr = `%*`(["/chalk", "exec", "--exec-command-name"]) - for item in lastEntryPoint.json.items(): - arr.add(item) - - newInstruction = "ENTRYPOINT " & $(arr) + newInstruction = "ENTRYPOINT " & formatExecArray(lastEntryPoint.json) else: # If we only have a CMD: # 1. shell form executes the full thing. @@ -360,14 +376,9 @@ proc rewriteEntryPoint*(ctx: DockerInvocation) = # If not, we'll have to explicitly skip it. if lastCmd.str != "": - newInstruction = "CMD exec /chalk exec --exec-command-name " & - lastCmd.str + newInstruction = "CMD " & formatExecString(lastCmd.str) else: - let arr = `%*`(["/chalk", "exec", "--exec-command-name"]) - for item in lastCmd.json.items(): - arr.add(item) - - newInstruction = "CMD " & $(arr) + newInstruction = "CMD " & formatExecArray(lastCmd.json) ctx.addedInstructions.add(newInstruction) info("Entry point wrapped.") diff --git a/src/commands/cmd_dump.nim b/src/commands/cmd_dump.nim index d1c2dc5b..4422c645 100644 --- a/src/commands/cmd_dump.nim +++ b/src/commands/cmd_dump.nim @@ -7,15 +7,71 @@ ## The `chalk dump` command. -import ../config, ../selfextract +import ../config, ../selfextract, unicode -proc runCmdConfDump*() = +const + configKey = "$CHALK_CONFIG" + paramKey = "$CHALK_SAVED_COMPONENT_PARAMETERS" + cacheKey = "$CHALK_COMPONENT_CACHE" + +template baseDump(code: untyped) {.dirty.} = var - toDump = defaultConfig + toDump: Rope + chalk = getSelfExtraction().getOrElse(nil) + extract = if chalk != nil: chalk.extract else: nil + + code + + print(toDump) + echo("") + quit(0) + +proc dumpToFile*(f: string) = + let chalk = getSelfExtraction().getOrElse(nil) extract = if chalk != nil: chalk.extract else: nil + config = if extract == nil or configKey notin extract: + defaultConfig + else: + unpack[string](extract[configKey]) + + publish("confdump", config) + quit(0) + +proc runCmdConfDump*() = + let args = getArgs() + + if len(args) > 0: + dumpToFile(args[0]) + baseDump: + var s: string + if chalk != nil and extract != nil and configKey in extract: + s = unpack[string](extract[configKey]) + else: + s = defaultConfig + + toDump = Rope(kind: RopeTaggedContainer, tag: "blockquote", + contained: Rope(kind: RopeAtom, text: s.toRunes())) + +proc runCmdConfDumpParams*() = + baseDump: + if chalk == nil or extract == nil or paramKey notin extract: + toDump = Rope(kind: RopeTaggedContainer, tag: "blockquote", + contained: Rope(kind: RopeAtom, text: "[]".toRunes())) + else: + toDump = boxToJson(extract[paramKey]).rawStrToRope(pre = false) + +proc runCmdConfDumpCache*() = + baseDump: + if chalk == nil or extract == nil or cacheKey notin extract: + runCmdConfDump() - if chalk != nil and extract != nil and extract.contains("$CHALK_CONFIG"): - toDump = unpack[string](extract["$CHALK_CONFIG"]) + let + componentInfo = selfChalk.extract[cacheKey] + unpackedInfo = unpack[OrderedTableRef[string, string]](componentInfo) - publish("confdump", toDump) + for url, contents in unpackedInfo: + toDump = toDump + htmlStringToRope("

URL: " & url & "

\n") + toDump = toDump + Rope(kind: RopeTaggedContainer, tag: "blockquote", + contained: Rope(kind: RopeAtom, + text: contents.toRunes())) diff --git a/src/commands/cmd_help.nim b/src/commands/cmd_help.nim index 26589652..799e5b8f 100644 --- a/src/commands/cmd_help.nim +++ b/src/commands/cmd_help.nim @@ -634,7 +634,7 @@ proc getConfigValues(): string = cols = [CcVarName, CcShort, CcCurValue] outConfFields = ["report_template", "mark_template"] cReportFields = ["enabled", "report_template", "use_when"] - sinkCfgFields = ["sink", "filters"] + #sinkCfgFields = ["sink", "filters"] plugFields = ["enabled", "codec", "priority", "ignore", "overrides"] confHdrs = ["Config Variable", "Description", "Current Value"] plugHdrs = ["Name", "Enabled", "Priority", "Ignore", "Overrides"] @@ -698,12 +698,12 @@ proc runChalkDocGen*() = con4mRuntime = getChalkRuntime() opts = CmdLineDocOpts(docKind: CDocRaw) - # 1. Dump embedded markdown docs. createDir(docDir) - for k, v in helpFiles: - f = newFileStream(docDir.joinPath(k) & ".md", fmWrite) - f.write(v) - f.close() + # 1. Dump embedded markdown docs. + #for k, v in helpFiles: + # f = newFileStream(docDir.joinPath(k) & ".md", fmWrite) + # f.write(v) + # f.close() # 2. Write out command docs. f = newFileStream(cmdline, fmWrite) diff --git a/src/commands/cmd_load.nim b/src/commands/cmd_load.nim index 8702ef01..a97d16d1 100644 --- a/src/commands/cmd_load.nim +++ b/src/commands/cmd_load.nim @@ -13,8 +13,6 @@ proc runCmdConfLoad*() = setContextDirectories(@["."]) initCollection() - var newCon4m: string - let url = getArgs()[0] if url == "0cool": diff --git a/src/commands/cmd_logout.nim b/src/commands/cmd_logout.nim index bc6ed796..c0551e58 100644 --- a/src/commands/cmd_logout.nim +++ b/src/commands/cmd_logout.nim @@ -7,7 +7,7 @@ ## The `chalk logout` command. -import ../api, ../collect, ../config, ../reporting, ../selfextract, ../util +import ../collect, ../config, ../reporting, ../selfextract proc runCmdLogout*() = info("Logging out of API, discarding saved tokens.") @@ -28,15 +28,15 @@ proc runCmdLogout*() = selfChalk.extract.del("$CHALK_API_KEY") #selfChalk.collectedData.del("$CHALK_API_KEY") info("Removed $CHALK_API_KEY successfully.") - + if "$CHALK_API_REFRESH_TOKEN" in selfChalk.extract: selfChalk.extract.del("$CHALK_API_REFRESH_TOKEN") #selfChalk.collectedData.del("$CHALK_API_REFRESH_TOKEN") info("Removed $CHALK_API_REFRESH_TOKEN successfully.") - + info("Updated configuration for " & selfChalk.name) selfChalk.writeSelfConfig() info("API logout successful.") - + doReporting() return diff --git a/src/commands/cmd_version.nim b/src/commands/cmd_version.nim index 6767a0e8..f5908191 100644 --- a/src/commands/cmd_version.nim +++ b/src/commands/cmd_version.nim @@ -10,7 +10,17 @@ import ../config proc runCmdVersion*() = - var txt = "" + var s = newStyle(lpad=4, rpad=4, borders = [BorderNone]) + s.useTopBorder = some(false) + s.useBottomBorder = some(false) + s.useLeftBorder = some(false) + s.useRightBorder = some(false) + s.useVerticalSeparator = some(false) + s.useHorizontalSeparator = some(false) + + styleMap["table"] = styleMap["table"].mergeStyles(s) + var txt = """
+ """ txt &= "" txt &= "" diff --git a/src/configs/README.md b/src/configs/README.md index bd032d19..9e87112e 100644 --- a/src/configs/README.md +++ b/src/configs/README.md @@ -2,13 +2,12 @@ This directory contains con4m code that Chalk uses for various purposes: | File | Purpose | | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| chalk.c42spec | This is a specification for what makes a valid chalk file. It is used to validate other files in this directory, and user configuration files, when provided. It contains definitions for the sections and fields allowed, and does extensive input validation. | -| baseconfig.c4m | This is where the default chalk metadata keys are set up, along with other defaults. | +| chalk.c42spec | This is a specification for what makes a valid chalk configuration. It is used to validate other files in this directory, and user configuration files, when provided. It contains definitions for the sections and fields allowed, and does extensive input validation. | +| base*.c4m | This is where the default chalk metadata keys are set up, along with other defaults. | | getopts.c4m | This specifies what is allowed at the command line, validates inputs and provides documentation for all the options. It's loaded together with baseconfig.c4m (as if #include'd in C). | | ioconfig.c4m | Sets up defaults for output and reporting. Run after the previous two, so that it can be influenced by command-line arguments. | -| signconfig.c4m | Sets up running external signing tools (currently only GPG). Whether this runs unless --no-load-sign-tools is passed at the command line. It too is run together with ioconfig.c4m. | | sbomconfig.c4m | Sets up external sbom collection tools, if --load-sbom-tools is passed. Also run with ioconfig.c4m | | sastconfig.c4m | Sets up external static analysis collection tools, if --load-sast-tools is passed, in which case it runs with ioconfig.c4m | -| defaultconfig.c4m | This is a 'default' user config file that runs, if no other user configuration file is embedded in the binary. It runs after the above, but before any on-filesystem config, if provided. | +| defaultconfig.c4m | This is a 'default' user config file that runs, if no other user configuration file is embedded in the binary. It runs after the above, but before any on-filesystem config, if provided. It doesn't actually do anything! | | dockercmd.c4m | This config file is used in parsing the _docker_ command line, or our container chalking and wrapping. It accepts a superset of valid docker command lines. | | entrypoint.c4m | This isn't a valid con4m file; it's a template for a valid con4m file. When wrapping docker entrypoints, this will be used to generate the configuration file we inject into the chalk binary to properly handle entry point execution. | diff --git a/src/configs/base_chalk_templates.c4m b/src/configs/base_chalk_templates.c4m index b4fce064..f9f50079 100644 --- a/src/configs/base_chalk_templates.c4m +++ b/src/configs/base_chalk_templates.c4m @@ -33,6 +33,7 @@ or vice versa. key.DATETIME_WHEN_CHALKED.use = true key.EARLIEST_VERSION.use = true key.HOSTINFO_WHEN_CHALKED.use = true + key.PUBLIC_IPV4_ADDR_WHEN_CHALKED.use = true key.NODENAME_WHEN_CHALKED.use = true key.INJECTOR_CHALK_ID.use = true key.INJECTOR_PUBLIC_KEY.use = true @@ -109,6 +110,7 @@ mark_template mark_large { key.DATETIME_WHEN_CHALKED.use = true key.EARLIEST_VERSION.use = true key.HOSTINFO_WHEN_CHALKED.use = true + key.PUBLIC_IPV4_ADDR_WHEN_CHALKED.use = true key.NODENAME_WHEN_CHALKED.use = true key.INJECTOR_CHALK_ID.use = true key.INJECTOR_VERSION.use = true diff --git a/src/configs/base_init.c4m b/src/configs/base_init.c4m index 693a2265..2378b732 100644 --- a/src/configs/base_init.c4m +++ b/src/configs/base_init.c4m @@ -25,7 +25,6 @@ docker { } load { } - cloud_provider { cloud_instance_hw_identifiers { } diff --git a/src/configs/base_keyspecs.c4m b/src/configs/base_keyspecs.c4m index fb787576..b0d8a98c 100644 --- a/src/configs/base_keyspecs.c4m +++ b/src/configs/base_keyspecs.c4m @@ -51,7 +51,7 @@ ## the more basic per-op stuff such as "_CHALKS" and "ACTION_ID" I did not. ## CHALK SCHEMA -chalk_version := "0.1.2" +chalk_version := "0.1.3" ascii_magic := "dadfedabbadabbed" # Field starting with an underscore (_) are "system" metadata fields, that @@ -237,6 +237,19 @@ the `uname()` system call. """ } +keyspec PUBLIC_IPV4_ADDR_WHEN_CHALKED { + kind: ChalkTimeHost + type: string + standard: true + since: "0.1.3" + shortdoc: "IPv4 address at Chalk time" + doc: """ +This returns the IPv4 address on the local machine used to route +external traffic. It's determined by setting up a UDP connection to +Cloudflare's public DNS service, but does not involve sending any data. +""" +} + keyspec NODENAME_WHEN_CHALKED { kind: ChalkTimeHost type: string @@ -3591,6 +3604,24 @@ the `uname()` system call. """ } +keyspec _OP_PUBLIC_IPV4_ADDR { + kind: RunTimeHost + type: string + standard: true + since: "0.1.3" + shortdoc: "IPv4 address" + doc: """ +This returns the IPv4 address on the local machine used to route +external traffic. It's determined by setting up a UDP connection to +Cloudflare's public DNS service, but does not involve sending any +data. + +There are other keys for reported IPs via other systems, including +cloud provider APIs, docker, procfs, etc. +""" +} + + keyspec _OP_NODENAME { kind: RunTimeHost type: string diff --git a/src/configs/base_plugins.c4m b/src/configs/base_plugins.c4m index 5e469cc6..e79eb527 100644 --- a/src/configs/base_plugins.c4m +++ b/src/configs/base_plugins.c4m @@ -15,8 +15,8 @@ plugin system { "INJECTOR_COMMIT_ID", "DATE_CHALKED", "TZ_OFFSET_WHEN_CHALKED", "DATETIME_WHEN_CHALKED", "INJECTOR_ENV", "HOSTINFO_WHEN_CHALKED", - "NODENAME_WHEN_CHALKED", "PLATFORM_WHEN_CHALKED", - "INJECTOR_PUBLIC_KEY"] + "PUBLIC_IPV4_ADDR_WHEN_CHALKED", "INJECTOR_PUBLIC_KEY", + "NODENAME_WHEN_CHALKED", "PLATFORM_WHEN_CHALKED"] artifact_keys: ["MAGIC", "OLD_CHALK_METADATA_HASH", "OLD_CHALK_METADATA_ID", "PRE_CHALK_HASH", "TIMESTAMP_WHEN_CHALKED"] @@ -27,10 +27,10 @@ plugin system { "_INVALID_SIGNATURE"] post_run_keys: ["_UNMARKED", "_OP_ERRORS", "_OPERATION", "_OP_SEARCH_PATH", - "_OP_HOSTINFO", "_OP_NODENAME", "_OP_PLATFORM", - "_OP_CHALKER_COMMIT_ID", "_OP_CHALKER_VERSION", - "_OP_CHALK_COUNT", "_OP_CMD_FLAGS", "_OP_EXE_NAME", - "_OP_EXE_PATH", "_OP_ARGV", "_OP_HOSTNAME", + "_OP_HOSTINFO", "_OP_PUBLIC_IPV4_ADDR", "_OP_NODENAME", + "_OP_PLATFORM", "_OP_CHALKER_COMMIT_ID", + "_OP_CHALKER_VERSION", "_OP_CHALK_COUNT", "_OP_CMD_FLAGS", + "_OP_EXE_NAME", "_OP_EXE_PATH", "_OP_ARGV", "_OP_HOSTNAME", "_OP_HOST_REPORT_KEYS", "_OP_UNMARKED_COUNT", "_TIMESTAMP", "_DATE", "_TIME", "_TZ_OFFSET", "_DATETIME", "_ENV"] diff --git a/src/configs/base_report_templates.c4m b/src/configs/base_report_templates.c4m index a89debfa..ca270720 100644 --- a/src/configs/base_report_templates.c4m +++ b/src/configs/base_report_templates.c4m @@ -29,6 +29,7 @@ report and subtract from it. key.DATETIME_WHEN_CHALKED.use = true key.EARLIEST_VERSION.use = true key.HOSTINFO_WHEN_CHALKED.use = true + key.PUBLIC_IPV4_ADDR_WHEN_CHALKED.use = true key.NODENAME_WHEN_CHALKED.use = true key.INJECTOR_CHALK_ID.use = true key.INJECTOR_PUBLIC_KEY.use = true @@ -303,6 +304,7 @@ report and subtract from it. key._OP_CHALKER_VERSION.use = true key._OP_PLATFORM.use = true key._OP_HOSTNAME.use = true + key._OP_PUBLIC_IPV4_ADDR.use = true key._OP_HOSTINFO.use = true key._OP_NODENAME.use = true key._OP_CLOUD_METADATA.use = true @@ -407,6 +409,7 @@ doc: """ key.DATETIME_WHEN_CHALKED.use = true key.EARLIEST_VERSION.use = true key.HOSTINFO_WHEN_CHALKED.use = true + key.PUBLIC_IPV4_ADDR_WHEN_CHALKED.use = true key.NODENAME_WHEN_CHALKED.use = true key.INJECTOR_CHALK_ID.use = true key.INJECTOR_VERSION.use = true @@ -442,6 +445,7 @@ doc: """ key._OP_CHALKER_VERSION.use = true key._OP_PLATFORM.use = true key._OP_HOSTNAME.use = true + key._OP_PUBLIC_IPV4_ADDR.use = true key._OP_HOSTINFO.use = true key._OP_NODENAME.use = true key._OP_CLOUD_METADATA.use = true @@ -533,6 +537,7 @@ doc: """ key.DATETIME_WHEN_CHALKED.use = true key.EARLIEST_VERSION.use = true key.HOSTINFO_WHEN_CHALKED.use = true + key.PUBLIC_IPV4_ADDR_WHEN_CHALKED.use = true key.NODENAME_WHEN_CHALKED.use = true key.INJECTOR_CHALK_ID.use = true key.INJECTOR_PUBLIC_KEY.use = true @@ -828,6 +833,7 @@ container. key.DATETIME_WHEN_CHALKED.use = true key.EARLIEST_VERSION.use = true key.HOSTINFO_WHEN_CHALKED.use = true + key.PUBLIC_IPV4_ADDR_WHEN_CHALKED.use = true key.NODENAME_WHEN_CHALKED.use = true key.INJECTOR_CHALK_ID.use = true key.INJECTOR_VERSION.use = true @@ -865,6 +871,7 @@ container. key._OP_CHALKER_VERSION.use = true key._OP_PLATFORM.use = true key._OP_HOSTNAME.use = true + key._OP_PUBLIC_IPV4_ADDR.use = true key._OP_HOSTINFO.use = true key._OP_NODENAME.use = true key._OP_CLOUD_METADATA.use = true @@ -1232,6 +1239,7 @@ and keep the run-time key. key.DATETIME_WHEN_CHALKED.use = false key.EARLIEST_VERSION.use = false key.HOSTINFO_WHEN_CHALKED.use = false + key.PUBLIC_IPV4_ADDR_WHEN_CHALKED.use = false key.NODENAME_WHEN_CHALKED.use = false key.INJECTOR_CHALK_ID.use = true key.INJECTOR_VERSION.use = true @@ -1267,6 +1275,7 @@ and keep the run-time key. key._OP_CHALKER_VERSION.use = true key._OP_PLATFORM.use = true key._OP_HOSTNAME.use = true + key._OP_PUBLIC_IPV4_ADDR.use = true key._OP_HOSTINFO.use = true key._OP_NODENAME.use = true key._OP_CLOUD_METADATA.use = true diff --git a/src/configs/chalk.c42spec b/src/configs/chalk.c42spec index f1f5f9ae..4e12e552 100644 --- a/src/configs/chalk.c42spec +++ b/src/configs/chalk.c42spec @@ -13,7 +13,7 @@ default_key_priority := 4611686018427387904 # 2^62. # These are the valid command-line commands. valid_chalk_cmds := ["help", "insert", "extract", "delete", "config", "load", "dump", "docker", "version", "env", "exec", - "setup", "login", "logout"] + "setup", "login", "logout", "docgen"] all_cmds_that_insert := ["insert", "build", "load", "setup", "login", "logout"] @@ -1423,7 +1423,9 @@ singleton load { gen_setters: false user_def_ok: false doc: """ -Options that control how the `chalk load` command works. +Options that control how the `chalk load` command works. Note that +these values are taken from the starting configuration, not any +configuration being loaded. """ field replace_conf { @@ -1447,7 +1449,6 @@ Otherwise, the passed configuration is treated like a component: This flag is ignored when running `chalk load default`, which will _always_ reset the embedded configuration to the default. - """ } @@ -1462,20 +1463,51 @@ Suppress validation of configuration files on loading. Please don't do this! field validation_warning { type: bool - default: true - shortdoc: "Show 'chalk load' validation warning" + default: false + shortdoc: "Show 'chalk load' validation warning" doc: """ Show the (admittedly verbose) warning you get when running 'chalk load'. +This is off by default, under the assumption that most people are going +to use the component system exclusively, and everyone else can read the +docs :) +""" + } + + field params_via_stdin { + type: bool + default: false + doc: """ +When this is on, loads will not use the interactive interface for +configuring parameters. Instead, chalk will read parameters from +stdin. + +Parameters should be in the format Chalk uses internally. You can +get the parameters via `chalk dump --params` or by setting the +configuration parameter `dump.params` """ } + + field update_arch_binaries { + type: bool + default: true + doc: """ +When this is true, if you run a `chalk load` on this binary, it will +try to (via docker) load the exact same configuration into any +cross-architecture binaries listed in docker.arch_binary_locations. + +Note that, if you source config components from a local directory, you +currently will need to update them manually, as those directories will +not be mapped into the container. +""" + } } singleton exec { -gen_fieldname: "execConfig" -gen_typename: "ExecConfig" -gen_setters: false -user_def_ok: false -doc: """ + gen_fieldname: "execConfig" + gen_typename: "ExecConfig" + gen_setters: false + user_def_ok: false + doc: """ When the `chalk docker` command wraps a container, it inserts a version of itself into the container, to be able to do data collection in the runtime environment. Although we do this by replacing the diff --git a/src/configs/getopts.c4m b/src/configs/getopts.c4m index a3361a3c..f7a00a51 100644 --- a/src/configs/getopts.c4m +++ b/src/configs/getopts.c4m @@ -9,7 +9,7 @@ default_recursive_doc := """ -"Determines whether a file scan will recursively walk paths to find artifacts. +Determines whether a file scan will recursively walk paths to find artifacts. """ getopts { @@ -132,31 +132,40 @@ This is similar to the `chalk config` command, except it shows state information flag_yn run_sbom_tools { field_to_set: "run_sbom_tools" doc: """ -For insertion operations, this flag forces running any configured tools for SBOM collection. It does not guarantee reporting or chalking; that is up to the reporting configuration. +For insertion operations, this flag forces running any configured +tools for SBOM collection. It does not guarantee reporting or +chalking; that is up to the reporting configuration. In the default chalk configuration, these tools do not run at all. -This flag is defined for all chalk commands, but currently is ignored for any command except "insert" or "docker". +This flag is defined for all chalk commands, but currently is ignored +for any command except "insert" or "docker". """ } flag_yn run_sast_tools { field_to_set: "run_sast_tools" doc: """ -For insertion operations, this flag forces running any configured tools for performing static analysis. It does not guarantee reporting or chalking; that is up to the reporting configuration. +For insertion operations, this flag forces running any configured +tools for performing static analysis. It does not guarantee reporting +or chalking; that is up to the reporting configuration. In the default chalk configuration, these tools do not run at all. -This flag is defined for all chalk commands, but currently is ignored for any command except "insert" or "docker". +This flag is defined for all chalk commands, but currently is ignored +for any command except "insert" or "docker". """ } flag_yn use_report_cache { field_to_set: "use_report_cache" doc: """ -Enables or disables the reporting cache. The reporting cache is a ring buffer stored locally, that contains reporting information that could not be delivered to its configured sources, due to some outage. +Enables or disables the reporting cache. The reporting cache is a +ring buffer stored locally, that contains reporting information that +could not be delivered to its configured sources, due to some outage. -When using the report cache, any time chalk does run reports, it will try to flush as much of the cache as it can. +When using the report cache, any time chalk does run reports, it will +try to flush as much of the cache as it can. """ } @@ -166,9 +175,12 @@ When using the report cache, any time chalk does run reports, it will try to flu field_to_set: "virtual_chalk" doc: """ -When chalking, do NOT modify artifacts, overriding anything defined in the config file. This is completely ignored for operations that do not normally modify artifacts. +When chalking, do NOT modify artifacts, overriding anything defined in +the config file. This is completely ignored for operations that do not +normally modify artifacts. -Specifically, this flag only works with `chalk insert`, `chalk docker build`, and `chalk delete`. +Specifically, this flag only works with `chalk insert`, `chalk docker +build`, and `chalk delete`. By default, this will write to "./virtual-chalk.json". """ @@ -178,9 +190,12 @@ By default, this will write to "./virtual-chalk.json". field_to_set: "chalk_debug" doc: """ -Shows nim stack traces where appropriate, generally where exceptions were caught. +Shows nim stack traces where appropriate, generally where exceptions +were caught. -Additionally, if temporary files might be useful to inspect, this causes them to not get deleted. Specifically, docker temporary files (most notably any docker file modifications) get left behind. +Additionally, if temporary files might be useful to inspect, this +causes them to not get deleted. Specifically, docker temporary files +(most notably any docker file modifications) get left behind. """ } @@ -190,11 +205,13 @@ Additionally, if temporary files might be useful to inspect, this causes them to no_aliases: [] doc: """ -Skip publishing the command report (i.e., the PRIMARY report). NO output sinks will get it. +Skip publishing the command report (i.e., the PRIMARY report). NO +output sinks will get it. _For most commands, this defeats the purpose of Chalk, so use it sparingly._ -Note that this doesn't turn off any custom reports; you have to disable those seprately. +Note that this doesn't turn off any custom reports; you have to +disable those seprately. """ } @@ -211,10 +228,13 @@ Whether to skip the summary report to the terminal. add_choice_flags: true field_to_set: "symlink_behavior" doc: """ -Chalk never follows directory links. When running non-chalking operations, chalk will read the file on the other end of the link, and report using the file name of the link. -

-For insertion operations, Chalk will, out of the box, warn on symbolic links, without processing them. -

+Chalk never follows directory links. When running non-chalking +operations, chalk will read the file on the other end of the link, and +report using the file name of the link. + +For insertion operations, Chalk will, out of the box, warn on symbolic +links, without processing them. + This variable controls what happens in those cases: - skip will not process files that are linked. @@ -232,7 +252,7 @@ containers will do a chalk report when they launch. Note that the 'docker' command passes through ALL flags, so this flag needs to technically be part of the 'global' flags, even though nothing else uses it. -

+ If, when wrapping, your chalk binary is using an external configuration file, that file will NOT get used inside the container. The wrapped binary currently only uses the embedded @@ -256,18 +276,25 @@ At the moment, this is only honored for the `chalk help` command. args: (0, high()) shortdoc: "Add chalk marks to artifacts" doc: """ -Add chalk marks to artifacts found on the file system. See the `docker` command for adding marks to docker containers. +Add chalk marks to artifacts found on the file system. See the +`docker` command for adding marks to docker containers. -On chalking, what gets put into the chalk mark will be determined by the active chalk mark template after any user config file has loaded. Each command's output configuration can be specified using the 'outconf' section in the configuration file. -

-For instance, if you create a new mark template named 'my_chalk_mark', you can activate it for both regular and docker insertions with the following in your configuration file: +On chalking, what gets put into the chalk mark will be determined by +the active chalk mark template after any user config file has loaded. +Each command's output configuration can be specified using the +'outconf' section in the configuration file. + +For instance, if you create a new mark template named 'my_chalk_mark', +you can activate it for both regular and docker insertions with the +following in your configuration file: ``` outconf.insert.chalk = "myconf" outconf.docker.chalk = "myconf" ``` -For information on mark templates on the command line, see: `chalk help templates` +For information on mark templates on the command line, see: `chalk +help templates` """ callback: func set_artifact_search_path(list[string]) @@ -443,29 +470,71 @@ The named reporting template must already exist in your configuration. aliases: [] shortdoc: "Show configuration variables and settings" doc: """ -Shows the results of evaluating the configuration, without actually doing any work with artifacts. -

-Even though they are related, there is a significant difference between the 'config' command and the --show-config flag. They both dump the configuration after evaluating any config file, but they may easily produce different results. -

-That's because chalk uses 'con4m' for configuration, which, while typically just looking like a regular config file, can have arbitrary code added, with conditionals, and so-on. The default configuration does, for instance, configure different output handlers, depending on the command given. -

-Running the 'defaults' command will therefore give you the information about the evaluation just when that command ran. Whereas, '--show-config extract' will dump the config as it resolves when you run the 'extract' command, which could be very similar, or very different. -

-Importantly though, running '--show-config extract' still runs the 'extract' command. -

-This command does not show the contents of the config file(s) used, just key results from executing those config files. And, generally there will be at least two 'stacked' configuration files. See 'help config' for more information on the configuration file and con4m. +Shows the results of evaluating the configuration, without actually +doing any work with artifacts. + +Even though they are related, there is a significant difference +between the 'config' command and the --show-config flag. They both +dump the configuration after evaluating any config file, but they may +easily produce different results. + +That's because chalk uses 'con4m' for configuration, which, while +typically just looking like a regular config file, can have arbitrary +code added, with conditionals, and so-on. The default configuration +does, for instance, configure different output handlers, depending on +the command given. + +Running the 'defaults' command will therefore give you the information +about the evaluation just when that command ran. Whereas, +'--show-config extract' will dump the config as it resolves when you +run the 'extract' command, which could be very similar, or very +different. + +Importantly though, running '--show-config extract' still runs the +'extract' command. + +This command does not show the contents of the config file(s) used, +just key results from executing those config files. And, generally +there will be at least two 'stacked' configuration files. See 'help +config' for more information on the configuration file and con4m. """ } command dump { args: (0, 1) - shortdoc: "Print the embedded configuration file" - doc: """ -Reads the embedded configuration file, and outputs it, based on your output configuration (see 'help output'). In the default configuration, if no argument is given, the config file is written to stdout; and if an argument is provided, it will try write the configuration to the file specified by the argument. -

-This behavior can be overridden by the configuration file, where you can specify different output configurations. See 'help config' for an overview of the configuration file format, and 'help output' for an overview of the output system. + arg_sub_mutex: false + shortdoc: "Print the embedded configuration file" + doc: """ +Reads the embedded configuration file, and outputs it, based on your +output configuration (see 'help output'). In the default +configuration, if no argument is given, the config file is written to +stdout; and if an argument is provided, it will try write the +configuration to the file specified by the argument. + +This behavior can be overridden by the configuration file, where you +can specify different output configurations. See 'help config' for an +overview of the configuration file format, and 'help output' for an +overview of the output system. +""" + command params { + shortdoc: "Output saved component parameters as JSON" + doc: """ +This does not output the saved configuration. Instead, it dump the +JSON for any saved parameters. That JSON can then be loaded into +another binary via chalk load --params, which takes parameters over +stdin, while setting a configuration. This is used in transfering +configurations to cross-platform binaries. +""" + } -""" + command cache { + shortdoc: "Output source for cached components" + doc: """ +This does a more complete dump of source code; not just the base +configuration, but also any cached components that have been loaded. +""" + + } } command load { @@ -490,6 +559,29 @@ provided argument. When off, it's used only as a component that's added to the config. """ } + + flag_yn update_arch_binaries { + field_to_set: "load.update_arch_binaries" + doc: """ +When this is true, if you run a `chalk load` on this binary, it will +try to (via docker) load the exact same configuration into any +cross-architecture binaries listed in docker.arch_binary_locations. + +Note that, if you source config components from a local directory, you +currently will need to update them manually, as those directories will +not be mapped into the container. +""" + } + + flag_yn params { + field_to_set: "load.params_via_stdin" + doc: """ +When provided, parameters will be taken from stdin, as a json +dictionary. Keys are the parameter name as specified in the +'parameter' block. If parameters that are needed aren't supplied, then +defaults will be accepted. +""" + } flag_yn validation { field_to_set: "load.validate_configs_on_load" @@ -501,7 +593,8 @@ When on, validate config files before loading them, by doing a trial run. flag_yn validation_warning { field_to_set: "load.validation_warning" doc: """ -This verbose flag controls whether or not you get the verbose warning. It's much better turning this off in your embedded configuration :) +This verbose flag controls whether or not you get the verbose warning. +It's much better turning this off in your embedded configuration :) """ } } @@ -658,7 +751,12 @@ command login { Starts the API login process and will provide a URL to follow in a browser to complete authentication. """ } - +command docgen { + shortdoc: "Generate technical documentation" + doc: """ +Internal function to generate technical documentation in markdown format. +""" + } } diff --git a/src/configs/ioconfig.c4m b/src/configs/ioconfig.c4m index 9dbdb8f9..b4d15453 100644 --- a/src/configs/ioconfig.c4m +++ b/src/configs/ioconfig.c4m @@ -38,11 +38,6 @@ if skip_summary_report { } cmd := command_name() -exceptions := ["defaults", "dump", "load", "profile", "version"] - -if exceptions.contains(cmd) { - subscribe("report", "json_console_out") -} subscribe("report", "default_out") subscribe("audit", "default_out") diff --git a/src/confload.nim b/src/confload.nim index df60fd21..d26e269f 100644 --- a/src/confload.nim +++ b/src/confload.nim @@ -19,8 +19,6 @@ import config, selfextract, con4mfuncs, plugin_load import macros except error -const chalkDefaultconfigStore = "https://chalkdust.io/" - # Since these are system keys, we are the only one able to write them, # and it's easier to do it directly here than in the system plugin. proc stashFlags(winner: ArgResult) = @@ -34,7 +32,7 @@ proc stashFlags(winner: ArgResult) = hostInfo["_OP_CMD_FLAGS"] = pack(flagStrs) -proc installComponentParams(params: seq[Box]) = +proc installComponentParams*(params: seq[Box]) = let runtime = getChalkRuntime() for item in params: @@ -119,8 +117,8 @@ proc loadLocalStructs*(state: ConfigState) = setCon4mVerbosity(c4errLevel) proc handleCon4mErrors(err, tb: string): bool = - if chalkConfig == nil or chalkConfig.chalkDebug: - error(err & "\n" & tb) + if tb != "" and chalkConfig == nil or chalkConfig.chalkDebug: + echo formatCompilerError(err, nil, tb, default(InstInfo)) else: error(err) return true @@ -157,8 +155,6 @@ proc loadAllConfigs*() = res: ArgResult # Used across macros above. resFound: bool - setDefaultStoreUrl(chalkDefaultConfigStore) - let toStream = newStringStream stack = newConfigStack() diff --git a/src/docs/core-hashing.md b/src/docs/core-hashing.md index 5f420fe3..ed027699 100644 --- a/src/docs/core-hashing.md +++ b/src/docs/core-hashing.md @@ -82,7 +82,7 @@ basically anywhere in the binary, but much like the Unix `strip` command, for the sake of simplicity and correctness, we move the section table to the back of the binary. Moreover, it would take significant additional work and require some storage to make this -operation invertable. +operation invertible. As a result, the Chalk Hash (the `HASH` metadata key), is not defined based on the file system hash. Instead, it is a _normalized_ hash, @@ -104,8 +104,8 @@ artifacts must be semantically identical. ### More on The Chalk ID -Once an artifact has been normalized, and the normalizated data stream -has been hashed using SHA-256, we programiatically take 100 bits of +Once an artifact has been normalized, and the normalized data stream +has been hashed using SHA-256, we programmatically take 100 bits of the raw hash output, base-32 encode those bits, and then add some hyphens for clarity, to get the `CHALK_ID`. diff --git a/src/docs/core-release-notes.md b/src/docs/core-release-notes.md index dd325f07..901a51a3 100644 --- a/src/docs/core-release-notes.md +++ b/src/docs/core-release-notes.md @@ -1,4 +1,48 @@ -# Release Notes for Chalk version 0.1.3 +# Release Notes for Chalk version 0.1.3 (Oct 19, 2023) + +## New Features + +- Added a module so that most users can easily install complex + configurations without editing any configuration information + whatsoever. Modules can be loaded from https URLs or from the local + file system. Our recipes will host modules on chalkdust.io. + + Modules can have parameters that you provide when installing them, + and can have arbitrary defaults (for instance, any module importing + the module for connecting to our demo web server defaults to your + current IP address). + + We do extensive conflict checking to ensure that modules that are + incompatible will not run (and generally won't even load). + + We will eventually do an in-app UI to browse and install modules. + [47](https://github.com/crashappsec/chalk/pull/47) + [67](https://github.com/crashappsec/chalk/pull/67) + +- Added initial metadata collection for GCP and Azure, along with a + metadata key to provide the current cloud provider, and a key that + distinguishes the cloud provider's environments. Currently, this + only does AWS (eks, ecs, ec2). + [59](https://github.com/crashappsec/chalk/pull/59) + [65](https://github.com/crashappsec/chalk/pull/65) + +- Added OIDC token refreshing, along with `chalk login` and + `chalk logout` commands to log out of auth for the secret manager. + [51](https://github.com/crashappsec/chalk/pull/51) + [55](https://github.com/crashappsec/chalk/pull/55) + [60](https://github.com/crashappsec/chalk/pull/60) + +- The initial rendering engine work was completed. This means + `chalk help`, `chalk help metadata` are fully functional. This engine is + effectively most of the way to a web browser, and will enable us to + offload a lot of the documentation, and do a little storefront (once + we integrate in notcurses). + [58](https://github.com/crashappsec/chalk/pull/58) + +- If you're doing multi-arch binary support, Chalk can now pass your + native binary's configuration to other arches, though it does + currently re-install modules, so the original module locations need + to be available. ## Fixes @@ -16,17 +60,24 @@ [39](https://github.com/crashappsec/chalk/pull/39) - Sometimes Docker build would not wrap entrypoint. [45](https://github.com/crashappsec/chalk/pull/45) +- Cosign now only gets installed if needed. + [49](https://github.com/crashappsec/chalk/pull/49) +- Docker `ENTRYPOINT`/`COMMAND` wrapping now preserves all named + arguments from original `ENTRYPOINT`/`COMMAND`. + (e.g. `ENTRYPOINT ["ls", "-la"]`) + [70](https://github.com/crashappsec/chalk/issues/70) ## Known Issues -### Containers +- There are still embedded docs that need to be fixed now that the + entire rendering engine is working well enough. -- When a `Dockerfile` does not use `USER` directive but base image - uses it to change default image user, chalk cannot wrap the - image as it on legacy Docker builder (not buildx) as it will - fail to `chmod` permissions of chalk during the build. +- When a `Dockerfile` does not use the `USER` directive but base image + uses it to change default image user, chalk cannot wrap the image as + it on legacy Docker builder (not buildx) as it will fail to `chmod` + permissions of chalk during the build. -# Release Notes for Chalk version 0.1.2 +# Release Notes for Chalk version 0.1.2 (Sept 26, 2023) This is the first open source release of Chalk. For those who participated in the public preview, there have been massive changes diff --git a/src/docs/core-secret-manager-api.md b/src/docs/core-secret-manager-api.md index 27db4459..6222a664 100644 --- a/src/docs/core-secret-manager-api.md +++ b/src/docs/core-secret-manager-api.md @@ -6,7 +6,7 @@ signing and attestation operations. All secrets and keying material are locally generated on the system running chalk, with the secret itself being encrypted -locally priot to being sent to the API. +locally prior to being sent to the API. This document provides an overview of the Secret Manager API, how data is stored securely, and how chalk interacts with the API as a @@ -125,7 +125,7 @@ open source. The encryption scheme makes use of a PRP using the Luby-Rackoff construction. The easiest thing for us to do is to break the input into two 'halves',one being 128 bits (the width of AES, which we -will call the 'lefthalf'), and the other the rest of the remaining +will call the 'left half'), and the other the rest of the remaining width of the input (the 'right half'). The nonce is random. @@ -145,13 +145,13 @@ generate a key stream, that we XOR into the right half. The other PRF is HMAC-3. We take the round key, HMAC the right side, truncate the result to 128 bits, then XOR into the left half. -The PRFs are used in a feistel cipher, so we alternate PRFs through -our four feistel rounds. +The PRFs are used in a Feistel cipher, so we alternate PRFs through +our four Feistel rounds. While three-round Luby-Rackoff is secure against some use cases, we go through the full four rounds. -PRPs are reversable, and with feistel contstructions, it's by +PRPs are reversible, and with Feistel construction, it's by running the rounds backward. Once constructed it is this encrypted value that is sent to the diff --git a/src/docs/guide-config-overview.md b/src/docs/guide-config-overview.md index f5c4c31a..7fac4d71 100644 --- a/src/docs/guide-config-overview.md +++ b/src/docs/guide-config-overview.md @@ -32,7 +32,7 @@ The exact metadata that will be getting included in a report are defined in _templates_ which are simply collections of metadata keys (with optional conditions on when said metadata should be getting emitted). The same template can be re-used across many reports, however each of the different reports -making use of the template could have different trigger/generation condidtions +making use of the template could have different trigger/generation conditions and different destinations. Here is an excerpt from the template used by default for any metadata extracted @@ -235,7 +235,7 @@ custom_report chalk_s3_logger { ``` -Notice that we have also suppreassed local terminal output for the above report. +Notice that we have also suppressed local terminal output for the above report. ### Updating the used templates diff --git a/src/docs/guide-getting-started.md b/src/docs/guide-getting-started.md index 70c06942..32b8d03b 100644 --- a/src/docs/guide-getting-started.md +++ b/src/docs/guide-getting-started.md @@ -12,9 +12,9 @@ CI/CD pipeline. In many cases, it can be completely transparent to the user. Any configuration should be done up-front by whoever needs the data -from chalk. While chalk is designed to be deeply customisable, we also +from chalk. While chalk is designed to be deeply customizable, we also worked hard to make out-of-the-box configurations useful, and to make -it very easy to configure common usecases. +it very easy to configure common use-cases. First, let's do some basics to get up and running, both with chalking artifacts, and reporting on them in production. @@ -239,7 +239,7 @@ to note for now: 1. We've captured basic information about the build environment, including our repo, branch and commit ID. If you pull a repo remotely - from Github or Gitlab, the "ORIGIN_URI" key will give the URL where + from GitHub or GitLab, the "ORIGIN_URI" key will give the URL where the repository is hosted, instead of `local`. 2. In addition to the report, we inserted a JSON blob into our @@ -583,7 +583,7 @@ without specifying a file name (which will just print to stdout): chalk dump ``` -Youu should see: +You should see: ```bash # The default config is empty. Please see chalk documentation for examples. @@ -751,7 +751,7 @@ code you're running. Chalk really only monitors a subset of docker commands, but when wrapping docker, it will pass through all docker commands even if it -doesn't do any of its own processing on them. If chalk encoounters an +doesn't do any of its own processing on them. If chalk encounters an error while attempting to wrap docker, it will then execute the underlying docker command without chalk so that this doesn't break any pre-existing pipelines. @@ -820,7 +820,7 @@ curl http://127.0.0.1:8585/execs # for pretty json output if you have jq installed, run `curl http://127.0.0.1:8585/execs | jq` ``` -![serverout](./img/execout.png){ loading=lazy } +![exec output](./img/execout.png){ loading=lazy } You can see that, in addition to artifact information, there is also information about the operating environment, including the container diff --git a/src/docs/guide-heartbeat.md b/src/docs/guide-heartbeat.md index 913f5031..4e8dd248 100644 --- a/src/docs/guide-heartbeat.md +++ b/src/docs/guide-heartbeat.md @@ -8,7 +8,7 @@ This document is a guide on how to configure chalk so that a chalked binary or docker container emits a snapshot of network connections at set intervals. -### Prerequisities +### Prerequisites - chalk binary - (optional) dockerfile for a docker image with compatible architecture diff --git a/src/docs/guide-user-guide.md b/src/docs/guide-user-guide.md index 725bfa3f..4ccbba9b 100644 --- a/src/docs/guide-user-guide.md +++ b/src/docs/guide-user-guide.md @@ -46,7 +46,7 @@ wizard, which we expect will meet most configuration needs. We will be making source code available at the time of our public launch. Instructions on how to build directly and building via docker -file are availabe in the [Chalk Getting Started +file are available in the [Chalk Getting Started Guide](./guide-getting-started.md), as well as instructions on how to download pre-built chalk binaries. @@ -249,7 +249,7 @@ will, by default: 3. Generate a chalk report with metadata on the build operation. Chalk also reports a bit of metadata when pushing images to help -provide full tracability. +provide full traceability. Chalk can also be configured to add build-time attestation when possible. @@ -367,7 +367,7 @@ Metadata is at the core of Chalk, which categorizes data into four types: 1. **Chalk-time artifact metadata**, which is data specific to a software artifact, collected when inserting chalk marks. This data can - be put into a chalk mark, and it can also be seprately reported + be put into a chalk mark, and it can also be separately reported without putting it in the chalk mark. 2. **Chalk-time host metadata**, which is data about the environment @@ -441,7 +441,7 @@ interoperability across implementations. For instance, it is easy to write a compliant chalk library that allows programs to store their implementations inside their -executable, and retrieve them, while still interoperating with other +executable, and retrieve them, while still inter-operating with other programs that collect a wider range of metadata. We certainly intend to allow other people to implement compatible @@ -531,7 +531,7 @@ modify chalk marks. Starting with Chalk 0.1.1, Chalk mark injectors that find an existing chalk mark in an artifact will, if replacing the chalk mark, keep `$` -keys they do not recognize, unless specificly configured to remove +keys they do not recognize, unless specifically configured to remove them, while also considering them part of the previous chalk mark. With Chalk 0.1.0, the `$CHALK_CONFIG` key is the only allowable key, @@ -593,7 +593,7 @@ keys will always be directly taken from the chalk mark. No keys without the leading underscore can be reported for non-insertion operations unless they are found in a chalk mark. -We do recommend, at chalk insertion time, to to be thoughtful about +We do recommend, at chalk insertion time, to be thoughtful about what metadata will be added to the chalk mark itself. There are two key reasons for this: @@ -609,7 +609,7 @@ There are two key reasons for this: in practice, some metadata objects may be quite large, such as generated SBOMs or static analysis reports. -The first concern is, by far, the most sigificant. Even in cases where +The first concern is, by far, the most significant. Even in cases where software never intentionally leaves an organization, there can be risks. For instance, if the chalk mark contains code ownership or other contact information, while it does make life easier for @@ -669,7 +669,7 @@ In the first case, the mark does NOT need to be at the end of the file, due to the support for placeholders. A valid placeholder consists of the JSON object `{ "MAGIC" : -"dadfedabbadabbed" }`. The presense of spaces and the number of spaces +"dadfedabbadabbed" }`. The presence of spaces and the number of spaces is all flexible, but no newlines are allowed. The intent here is to allow developers to specify where they want @@ -694,8 +694,8 @@ solution. Currently, we're considering two approaches: 1. File-based artifacts will need to be scanned in their entirety before marking, and if a mark is found, the spot is reused. This would - make things easier on implementators, but could impact performance for - some larger artifiacts. + make things easier on implementors, but could impact performance for + some larger artifacts. 2. We may require marking the locations that older versions would have selected with a mark that invalidates the location, and points to the @@ -763,7 +763,7 @@ well-defined image format is not allowed. ### Replacing existing marks When a Chalk mark already exists in a document, it's up to the context -of the insertion whether the the existing chalk mark should be +of the insertion whether the existing chalk mark should be removed. In most cases, an existing chalk mark should be preserved. For instance, when chalking during deployment, any previous chalk mark from the build process should be preserved. @@ -788,7 +788,7 @@ strongly discourage using those keys without reporting. Extractors generally do not need to care about file structure for non-image formats. It should be sufficient for them to scan the bytes -of such artifacts, looking for the existance of Chalk `MAGIC` key. +of such artifacts, looking for the existence of Chalk `MAGIC` key. However, for image-based formats, the extractor needs to be aware enough of the marking requirements for that format to be able to @@ -882,7 +882,7 @@ For more information, see the following: fields. Documentation for keys will also include the conditions where the reference implementation can find them. - [The Config Overview Guide](./guide-config-overview.md) covers how - to to configure WHERE reports get sent. + to configure WHERE reports get sent. Note that compliant insertion implementations do not require compliant reporting implementations. But compliant chalk tools for other @@ -890,7 +890,7 @@ operations MUST produce fully conformant JSON. However, there are no requirements on how that JSON gets distributed or managed, other than that compliant implementations must provide a -straightforward way to make the JSON avilable to users if desired. +straightforward way to make the JSON available to users if desired. A report not in the proper format, or with key/values pairs that are not compliant, is not a Chalk report. @@ -926,7 +926,7 @@ The normalization algorithm is as follows: `TZ_OFFSET`, `DATETIME`. 3. The following key/value pair is encoded LAST, (whenever present): `ERR_INFO`. -4. The remaining keys are encoded in lexigraphical order. +4. The remaining keys are encoded in lexicographical order. 5. The encoding starts with the number of keys in the normalization, as a 32-bit little endian integer. 6. Each key/value pair is encoded in order by encoding the key, and @@ -974,7 +974,7 @@ validation discussed below built on top of the `METADATA_ID`. We currently omit `EMBEDDED_CHALK`, instead allowing them to be independently validated, if desired. While this does mean the `EMBEDDED_CHALK` key can be excised without detection at validation -time, we expect that either the relevent sub-artifacts will have +time, we expect that either the relevant sub-artifacts will have embedded chalk marks themselves, or the server will have record of the insertion. @@ -1000,12 +1000,12 @@ well, as long as there is a `HASH` field). In containers, where we do not have an easy, reliable hash, metadata normalization and validation works the same way. But we strongly -recommend automatic digitial signatures to ensure that you can detect +recommend automatic digital signatures to ensure that you can detect changes to the container. Digital signing can be used both with containers and with other artifacts. With containers, we use Sigstore with their In-Toto -attestations that we appply on `docker push`. The mark is replicated +attestations that we apply on `docker push`. The mark is replicated in full inside the attestation. For other artifacts, the signature is stored in the Chalk mark, but is @@ -1044,7 +1044,7 @@ docker.label_prefix: "com.example." ``` In the configuration file, we can also set up environment variables -for reporting, such as by defining new environment variablaes and +for reporting, such as by defining new environment variables and using simple if / else logic to set a default if the environment variable is not set on the host. For example: @@ -1135,7 +1135,7 @@ reporting on any of those keys. | Artifact | Any software artifact handled by Chalk, which can recursively include other artifacts. For instance, a Zip file is an artifact type that can currently be chalked, which can contain ELF executables that can also be chalked. | | Chalk Mark | JSON containing metadata about a software artifact, generally inserted directly into the artifact in a way that doesn’t affect execution. Often, a chalk mark will be minimal, containing only small bits of identifying information that can be used to correlate the artifact with other metadata collected. | | Unchalked | A software artifact that does not have a chalk mark embedded in it. | -| Metadata Key | Each piece of metadata Chalk is able to collect (metadata being data about an artifact or a host on which an artifact has been found) is associated with a metadata key. Chalk reports all metadata in JSon key/value pairs, and you specify what gets added to a chalk mark and what gets reported on by listing the metadata keys you’re interested in via the report template and mark emplate. | +| Metadata Key | Each piece of metadata Chalk is able to collect (metadata being data about an artifact or a host on which an artifact has been found) is associated with a metadata key. Chalk reports all metadata in JSon key/value pairs, and you specify what gets added to a chalk mark and what gets reported on by listing the metadata keys you’re interested in via the report template and mark template. | | Chalking | The act of adding metadata to a software artifact. Aka, “insertion”. | | Extraction | The act of reading metadata from artifacts and reporting on them. | | Report | Every time Chalk runs, it will want to report on its activity. That can include information about artifacts, and also about the host. Reports are “published” to output “sinks”. By default, you’ll get reports output to the console, and written to a local log file, but can easily set up HTTPS post or writing to object storage either by supplying environment variables, or by editing the Chalk configuration. | @@ -1143,6 +1143,6 @@ reporting on any of those keys. | Mark Template | Like report templates, you have complete flexibility over what goes into chalk marks. A mark template is a specification of metadata keys that you want to go into the chalk mark. | | Sinks | Output types handled by Chalk. Currently, chalk supports JSON log files, rotating (self-truncating) JSON log files, s3 objects, http/https post, and stdin/stdout. | | Chalk ID | A value unique to an unchalked artifact. Usually, it is derived from the SHA-256 hash of the unchalked artifact, except when that hash is not available at chalking time, in which case, it’s random. Chalk IDs are 100 bits, and human readable (Base32). | -| Metadata ID | A value unique to a chalked artifact. It is always derived from a normalized hash of all other metadata (except for any metadata keys involved in signing the Metadata ID). Metdata IDs are also 100 bits, and Base32 encoded. | +| Metadata ID | A value unique to a chalked artifact. It is always derived from a normalized hash of all other metadata (except for any metadata keys involved in signing the Metadata ID). Metadata IDs are also 100 bits, and Base32 encoded. | | Chalkable keys | Metadata keys that can be added to chalk marks. When reported for an artifact (e.g., during extraction in production), they will always indicated chalk-time metadata. | | Non-chalkable keys | Metadata keys that will NOT be added to chalk marks. They will always be reported for the current operation, and start with a `_`. There are plenty of metadata keys that have chalkable and non-chalkable versions. | diff --git a/src/docs/howto-app-inventory.md b/src/docs/howto-app-inventory.md index 00fff409..1b487a20 100644 --- a/src/docs/howto-app-inventory.md +++ b/src/docs/howto-app-inventory.md @@ -15,16 +15,13 @@ figure out where the code lives and who owns it. Similarly, developers often would like to know what versions of their code are deployed where, especially when a bug report comes in. -This how-to uses Chalk™ to automate this in five steps: +This how-to uses Chalk™ to automate this easily: -1. Load our `app-inventory` configuration -2. Set up the Inventory web service -3. Configure where Chalk reports get sent +1. Start a web service to collect data (via docker) +2. Load our `app-inventory` configuration +3. (Optional) Start up a service to let us browse collected data. -4. Automate calling docker via `chalk` in your build environment. -5. Use it - -## Steps +Each of the steps involves running only a single command. ### Before you start @@ -32,186 +29,108 @@ The easiest way to get Chalk is to download a pre-built binary from our [release page](https://crashoverride.com/releases). It's a self-contained binary with no dependencies to install. -### Step 1: Load our `app-inventory` configuration - -Chalk is designed so that you can easily pre-configure it for the -behavior you want, so that you can generally just run a single binary -with no arguments, to help avoid using it wrong. - -We're going to download and install a chalk configuration that does -the following: - -1. Sets up Chalk to be able to seamlessly wrap invocations of Docker - via a global alias. - -2. Configures Chalk to report not only build-time information, but - runtime information when containers built with this recipe are run. - -3. Has everything report back to a container we'll deploy in the next step. +Additionally, the reporting web service we'll install by running two +docker containers, one for collecting logs, and the other to give us a +web frontend to browse them. -The container we'll deploy is a simple Python-based HTTP server -integrated with SQLite. You'll be able to browse and search all the -info you collect with SQL, or by adding any frontend you desire. +### Step 1: Set up the Inventory web service -Or, you can easily use any HTTP / HTTPS endpoint you like. +We've put together a simple Python-based API Server that will accept +reports from the chalk binary we're configuring, and stick things +in an SQLite database. -The base configuration for this recipe though, will assume the -reporting container is always running on 'localhost:8585'. +The SQLite database will live in `~/.local/c0/chalkdb.sqlite`. -We can fix that after we get things up and running. For now, let's -just install the base. - -Assuming that you've downloaded Chalk, and it's in your current -directory, you would simply run: +To start up the API server, which will create our database, run: ```bash -./chalk load https://chalkdust.io/app-inventory.c4m +docker run --rm -d -w /db -v $HOME/.local/c0/:/db -p 8585:8585 --restart unless-stopped ghcr.io/crashappsec/chalk-test-server ``` -This downloads our config, tests it, and loads it into the binary. +This will set up an API server on port 8585 on your machine, +accessible from any interface. Note, it will run in the background. -Note that Chalk reconfigures itself by editing its binary. So it's -best when configuring to have write access to the binary. If you do -not, then copy the binary and run it from someplace you do. +### Step 2: Load our `app-inventory` configuration -### Step 2: Set up the Inventory web service +Chalk can load remote modules to reconfigure functionality. Even if +you've already configured Chalk, you should simply just run: -We are going to set up two containers: +``` +./chalk load https://chalkdust.io/app_inventory.c4m +``` -1. A simple Python-based API Server that will accept reports from the - chalk binary we're configuring, and stick things in the SQLite - database. +You will be prompted to enter the IP address for the server we set up +in the previous step. The default will be your personal IP +address. For instance, I get: -2. A container running an SQLite Web interface to give us a reasonable - GUI on top of it. +![Output 1](../img/appinv-ss1.png) -Both of these images will need to share a single SQLite database. The -API server we'll want to configure to listen for connections on -external interfaces. +Generally, the default should work just fine. -Then, in the next step, we're going to want to re-configure our Chalk -binary to use the public IP address of the container. +After accepting the binary, it'll prompt you one more time to finish +the setup. The resulting binary will be fully configured, and can be +taken to other machines, as long as your server container stays up. -Let's put our SQLite database in `~/.local/c0/chalkdb.sqlite`. +There's nothing else you need to do to keep this new configuration-- +Chalk rewrites data fields in its own binary when saving the +configuration changes. -First, let's start up the API server, which will create our database -for us: +### Step 3: Browse some data! -```bash -docker run \ - --rm \ - -d \ - -w /db \ - -v $HOME/.local/c0/:/db \ - -p 8585:8585 \ - ghcr.io/crashappsec/chalk-test-server -``` +Now, we should build and deploy some containers using Chalk, so you +can see real data in the database. -This will set up an API server on port 8585 on your machine, -accessible from any interface. Note, it will run in the background. -You can verify the healthcheck of the server by running +As a really simple example, let's build a container that prints load +averages once a minute to stdout. +First, we'll write a script for this: ```bash -curl http://localhost:8585/health +cat > example.sh < Dockerfile < 💀 We do _not_ recommend /etc/profile.d because some (non-login) -> shells will not use this. - -Once you add this, you can log out and log back in to make the alias -take effect, our simply `source` the file: +If you're not an SQLite expert, we can run a web service that points +to the same database, that makes it a bit easier to browse. +Let's set it up on port 8080: ```bash -source /etc/bash.bashrc +docker run -d -p 3000:3000 -p 3001:3001 -v $HOME/.local/c0/chalkdb.sqlite:/chalkdb.sqlite lscr.io/linuxserver/sqlitebrowser:latest ``` -Now, whenever a new bash shell gets created that starts a `docker` -process, they'll be automatically configured to call `chalk` -instead. The way we've configured `chalk`, when it doesn't see any of -its own commands, it knows to use the Chalk `docker` command. - -That command always runs the Docker command intended by the user, but -in our case: +The database GUI will be available on port 3000. But, our database +will be empty until we start using Chalk, so definitely use chalk to +build and deploy some workloads. -1. Collects information about the build environment; and -2. Slightly adjusts the Docker input so that Chalk will also start up - with containers, and report to your Inventory web service. - -### Step 5: Use it - -Build and deploy some workloads. Once you do, from the machine you -deployed the containers, browse SQLite database at +Now, you can browse your SQLite database at [http://localhost:8080](http://localhost:8080). The database will be capturing both the repositories you're using to @@ -228,38 +147,37 @@ information AND the containers you deploy. > wrapping `docker push`, but you'll have to go through extra work to > link them together; the CHALK_ID will work. -In addition to manually browsing the SQLite database, you can query -some of the data via the API. - -To list all built docker images you can list all chalk marks: - -```bash -curl "http://localhost:8585/chalks" -s | jq -``` - -To see all `exec` chalk reports from running containers: - -```bash -curl "http://localhost:8585/reports?operation=exec" -s | jq -``` - -To see all available endpoints you can see the Swagger docs of the API -at [http://localhost:8585/docs](http://localhost:8585/docs) +If you like Chalk, you can easily deploy across your docker builds and +deploys by adding a global alias. See the [howto for docker deployment](./howto-deploy-chalk-globally-using-docker.md) ## Warning -This how-to was written for local demonstration purposes only.There is no security for this how-to. You should always have authn, authz and uses SSL as an absolute minimum. +This how-to was written for local demonstration purposes only.There is +no security for this how-to. You should always have authn, authz and +uses SSL as an absolute minimum. ## Our cloud platform -While creating a basic app inventory with Chalk is easy, our cloud platform makes it even easier. It is designed for enterprise deployments, and provides additional functionality including prebuilt configurations to solve common tasks, prebuilt integrations to enrich your data, a built-in query editor, an API and more. +While creating a basic app inventory with Chalk is easy, our cloud +platform makes it even easier. It is designed for enterprise +deployments, and provides additional functionality including prebuilt +configurations to solve common tasks, prebuilt integrations to enrich +your data, a built-in query editor, an API and more. -There are both free and paid plans. You can [join the waiting list](https://crashoverride.com/join-the-waiting-list) for early access. +There are both free and paid plans. You can [join the waiting +list](https://crashoverride.com/join-the-waiting-list) for early +access. ### Background Information -Traditionally IT departments maintained list of their hardware and software assets in a CMDB or [configuration management data base](https://en.wikipedia.org/wiki/Configuration_management_database). These systems were not designed for modern cloud based software and the complexity of code that they are made from. +Traditionally IT departments maintained list of their hardware and +software assets in a CMDB or [configuration management data +base](https://en.wikipedia.org/wiki/Configuration_management_database). These +systems were not designed for modern cloud based software and the +complexity of code that they are made from. -Spotify created a project called [Backstage](https://backstage.io) to centralise developer documentation. Many companies now use it as a source of truth for their development teams. +Spotify created a project called [Backstage](https://backstage.io) to +centralise developer documentation. Many companies now use it as a +source of truth for their development teams. -Many companies create application inventories using spreadsheets. +Many companies create application inventories using spreadsheets. \ No newline at end of file diff --git a/src/docs/howto-compliance.md b/src/docs/howto-compliance.md index 2822a5c4..19fc2d85 100644 --- a/src/docs/howto-compliance.md +++ b/src/docs/howto-compliance.md @@ -20,9 +20,11 @@ This information is complex and tedious to generate, and manage. This how-to uses Chalk™ to automate this in two steps: -1. Configure chalk to generate SBOMs, collect code provenance data, and digitally sign it +1. Load our basic compliance configuration. -2. Configure chalk to automatically generate compliance reports +2. Turn on signing. + +3. Build software using Docker. As a big bonus, with no extra effort, you can be [SLSA](https://slsa.dev) [level 2](https://slsa.dev/spec/v1.0/levels) compliant, before people start officially requiring SLSA [level 1](https://slsa.dev/spec/v1.0/levels) compliance. @@ -41,40 +43,28 @@ Chalk is designed so that you can easily pre-configure it for the behavior you want, and so that you can generally just run a single binary with no arguments, to help avoid using it wrong. -Therefore, for this how-to, you should configure your binary with -either our `compliance-docker` configuration, or our -`compliance-other` configuration, depending on whether you're using -docker or not. - -Assuming you've downloaded chalk into your working directory, in the -docker case, you would run: - -``` -./chalk load https://chalkdust.io/compliance-docker.c4m -``` - -Otherwise, run: +Assuming you've downloaded chalk into your working directory, you just +need to run: ``` -./chalk load https://chalkdust.io/compliance-other.c4m +./chalk load https://chalkdust.io/compliance_docker.c4m ``` -The profile we've loaded changes only three things from the default +The profile we've loaded changes two key things from the default behavior: 1. It enables the collection of SBOMS (off by default because on large projects this can add a small delay to the build) -2. Specifies that any SBOM produced should be added to the chalk mark. - -3. It configures the default action for the binary, when no specific - command is applied (this is the only difference between the two - configurations). +2. Specifies that any SBOM produced should be added to built +artifacts. By default, chalk is already collecting provenance information by examining your project's build environment, including the .git directory and any common CI/CD environment variables. +### Step 2: Turn on signing + To setup digital signing we have built yet another easy button. Simply run: @@ -95,48 +85,23 @@ At this point, your chalk binary will have re-written itself to contain most of what it needs to sign, except for a `secret` that it requests dynamically from our secret service. -### Step 2: Configure chalk to automatically generate compliance reports +### Step 3: Build software -Now that the binary is configured, you probably will want to move -the `chalk` binary to a system directory that's in your `PATH`. +Now that the binary is configured, you probably may want to move the +`chalk` binary to a system directory that's in your `PATH`. If you're +running Docker, we recommend adding a global alias, so that Chalk +always runs, See the [howto for docker deployment](./howto-deploy-chalk-globally-using-docker.md) How you run chalk depends on whether you're building via `docker` or not: - _With docker_: You "wrap" your `docker` commands by putting the - word `chalk` in front of them. - -- _Without docker_ : You simply invoke `chalk` in your build pipeline. - -As configured, anyone with access to the artifact can use chalk to not -only see the chalk mark, but to validate the signature. + word `chalk` in front of them. That's it. -Any build of chalk can extract the chalk mark, and verify -everything. While we configured our binary to add marks by default, -the `extract` command will pull them out and verify them. +- _Without docker_ : You simply invoke `chalk insert` in your build + pipeline. It defaults to inserting marks into artifacts in your + current working directory. -By default, it will look on the file system in the same way that -insertion did when we weren't using Docker. So: - -``` -chalk extract -``` - -Chalk extract will report on anything it finds under your current working directory, -And so will extract the mark from any artifacts you chalked that weren't -containerized. But if you built the example container, we can extract -from the example container easily by just by providing a reference to it: - -``` -chalk extract ghcr.io/viega/wordsmith:latest -``` - -Either will pull all the metadata chalk saved during the first -operation, and log it, showing only a short summary report to your -console. If the signature validation fails, then you'll get an obvious -error! If anyone tampers with a mark, or changes a file after the -chalking, it's easily detected. - -#### Step 2a: Docker +#### Step 3a: Docker Your `build` operations will add a file (the _"chalk mark"_) into the container with provenance info and other metadata, and any SBOM @@ -168,7 +133,7 @@ use features that `chalk` doesn't understand), the program will _always_ makes sure the original `docker` command gets run if it doesn't successfully exit when wrapped. -#### Step 2b: When Not Using Docker +#### Step 3b: When Not Using Docker When you invoke chalk as configured, it searches your working directory for artifacts, collects environmental data, and then injects @@ -191,11 +156,39 @@ file. JAR files (and other artifacts based on the ZIP format) are handled similarly to container images, and there are marking approaches for a few other formats, with more to come. +### What to tell other people + +Any `chalk` executable can extract the chalk mark, and verify +everything. While we configured our binary to add marks by default, the +`extract` command will pull them out and verify them. + +Chalk extract will report on anything it finds under your current +working directory, And so will extract the mark from any artifacts you +chalked that weren't containerized. But if you built the example +container, we can extract from the example container easily (or any +other container you have a local copy of) by just by providing a +reference to it: + +``` +chalk extract ghcr.io/viega/wordsmith:latest +``` + +Either will pull all the metadata chalk saved during the first +operation, and log it, showing only a short summary report to your +console. If the signature validation fails, then you'll get an obvious +error! If anyone tampers with a mark, or changes a file after the +chalking, it is clear in the output. + ## Our cloud platform -While creating compliance reports with chalk is easy, our cloud platform makes it even easier. It is designed for enterprise deployments, and provides additional functionality including prebuilt configurations to solve common tasks, prebuilt integrations to enrich your data, a built-in query editor, an API and more. +While creating compliance reports with chalk is easy, our cloud +platform makes it even easier. Not only can you collect software +compliance information automatically, you can easily share it with +anyone who needs it. -There are both free and paid plans. You can [join the waiting list](https://crashoverride.com/join-the-waiting-list) for early access. +There are both free and paid plans. You can [join the waiting +list](https://crashoverride.com/join-the-waiting-list) for early +access. ### Background information @@ -306,7 +299,8 @@ provide out-of-band. That's why, when you ran `chalk setup`, Chalk output to disk two files: 1. _chalk.pub_ The public key you can give people out of band. - 2 _chalk.pri_ The ENCRYPTED private key (just in case you want to load + +2 _chalk.pri_ The ENCRYPTED private key (just in case you want to load the same key into a future chalk binary). When you run `chalk setup`, we generate a keypair for you, and encrypt @@ -409,4 +403,4 @@ Log4J is a popular logging library for the Java programming language. In late 20 The SolarWinds attack used an IT monitoring system, Orion, which which had over 30,000 organizations including Cisco, Deloitte, Intel, Microsoft, FireEye, and US government departments, including the Department of Homeland Security. The attackers created a backdoor that was delivered via a software update. -[The Untold Story of the Boldest Supply-Chain Hack Ever](https://www.wired.com/story/the-untold-story-of-solarwinds-the-boldest-supply-chain-hack-ever/) - Wired Magazine +[The Untold Story of the Boldest Supply-Chain Hack Ever](https://www.wired.com/story/the-untold-story-of-solarwinds-the-boldest-supply-chain-hack-ever/) - Wired Magazine \ No newline at end of file diff --git a/src/docs/howto-deploy-chalk-globally-using-docker.md b/src/docs/howto-deploy-chalk-globally-using-docker.md new file mode 100644 index 00000000..f99f3539 --- /dev/null +++ b/src/docs/howto-deploy-chalk-globally-using-docker.md @@ -0,0 +1,92 @@ +# Deploy Chalk globally via Docker + +### Automatically get visibility for every Docker build + +## Summary + +One of the biggest challenges to automatically tying together +information you have about production to information you have about +source code is the ease of deployment at scale. + +Nobody wants to deploy one repository at a time, and if you do ask +people to add things to their pipelines, it will probably be forgotten +or misused. + +With Chalk™, when your teams build via Docker, you can easily set up +Chalk on your build systems to automatically operate on every docker +build. All you need to do is: + +1. Install a configured Chalk binary. +2. Set up a global alias for docker, having it call Chalk. + +That's it. Chalk figures the rest out. + +## Steps + +### Step 1: Install a configured binary. + +The easiest way to get Chalk is to download a pre-built binary from +our [release page](https://crashoverride.com/releases). It's a +self-contained binary with no dependencies to install. + +Configuring Chalk is also easy. For the sake of example, we will use +our [compliance configuration](./howto-compliance.md). + +If Chalk is in your current directory, run: + +``` +./chalk load https://chalkdust.io/compliance-docker.c4m +``` + +When you install Chalk on your build systems, we recommend putting it +in the same directory where your docker executable is, though anywhere +in the default PATH is fine. + +### Step 2: Add a global alias + +You _could_ now deploy chalk and ask everyone to run it by invoking +`chalk` before their docker commands. But that's easy to forget. It's +really better to automatically call `chalk` when invoking Docker. + +You can do this easily with a global alias. Your build systems will +have a global file for bash configuration, which, these days, is +almost always `/etc/bash.bashrc` (but if it's not there, then it +should be at`/etc/bashrc`). + +This file runs when any bash shell starts. All you need to add to this +file is: + +```bash +alias docker=chalk +``` + +> 💀 Some people add global aliases to /etc/profile.d, but we do _not_ recommend this, because some (non-login) shells will not use this. + +Once you add this, you can log out and log back in to make the alias +take effect, our simply `source` the file: + +```bash +source /etc/bash.bashrc +``` + +Now, whenever a new bash shell gets created that starts a `docker` +process, they'll be automatically configured to call `chalk` +instead. The way we've configured `chalk`, when it doesn't see any of +its own commands, it knows to use the Chalk `docker` command. + +We always run the Docker command intended by the user, but we also +collect and report on environmental info. + +You can also ask Chalk to add automatic data reporting on startup to +built containers ig you like, as described in [our how-to on building +an application inventory](./howto-app-inventory.md) + +## Our cloud platform + +We have tried to make doing everything with Chalk as easy as possible, our cloud +platform makes it even easier. It is designed for enterprise +deployments, and provides additional functionality including prebuilt +configurations to solve common tasks, prebuilt integrations to enrich +your data, a built-in query editor, an API and a lot more. + +There are both free and paid plans. You can [join the waiting list](https://crashoverride.com/join-the-waiting-list) for early access. \ No newline at end of file diff --git a/src/docs/howto-net-services.md b/src/docs/howto-net-services.md index 90b705d4..d7681dbd 100644 --- a/src/docs/howto-net-services.md +++ b/src/docs/howto-net-services.md @@ -139,7 +139,7 @@ configured reporting. You should see some additional JSON output from `chalk` after the build finishes, identifying the metadata information for the newly -chalked contianer: +chalked container: ```json [ @@ -187,7 +187,7 @@ chalked contianer: If you built your container with the commands above, you should now be able to now run it with: `docker run --rm -it mychalkedcontainer` -Also, if you kept the the `output_to_screen` sink to be `enabled: +Also, if you kept the `output_to_screen` sink to be `enabled: true`, and set the heartbeat window to 10 seconds, then after 10 seconds you should see output similar to the following: diff --git a/src/docs/img/appinv-ss1.png b/src/docs/img/appinv-ss1.png new file mode 100644 index 0000000000000000000000000000000000000000..cae159134c115cddef3a4281e03c60e6c88e15a4 GIT binary patch literal 66233 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%0D4K&q2j}tR? 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

Chalk version" & getChalkExeVersion() & "
Commit ID" & getChalkCommitID() & "