├── .gitignore ├── Dockerfile ├── Gemfile ├── LICENSE ├── Makefile ├── README.md ├── action.yml ├── images └── diagram.png ├── input.yml ├── profiles ├── README.md ├── all │ ├── .gitignore │ ├── README.md │ ├── controls │ │ ├── .gitkeep │ │ └── all.rb │ └── inspec.yml ├── consul │ ├── README.md │ ├── controls │ │ ├── config.yaml │ │ └── validate.rb │ └── inspec.yml ├── github │ ├── .gitignore │ ├── README.md │ ├── controls │ │ ├── .gitkeep │ │ └── action.rb │ └── inspec.yml ├── nomad.io │ ├── README.md │ ├── controls │ │ ├── config.yaml │ │ └── validate.rb │ └── inspec.yml ├── nomad │ ├── README.md │ ├── controls │ │ ├── config.yaml │ │ └── validate.rb │ └── inspec.yml ├── packer │ ├── README.md │ ├── controls │ │ ├── config.yaml │ │ └── validate.rb │ ├── inspec.yml │ └── libraries │ │ └── packer_syntax.rb ├── shared │ ├── .gitignore │ ├── README.md │ ├── controls │ │ ├── .gitkeep │ │ └── shared.rb │ ├── inspec.yml │ └── libraries │ │ ├── README.md │ │ ├── hcl_syntax.rb │ │ ├── json_syntax.rb │ │ ├── shell_syntax.rb │ │ └── yaml_syntax.rb ├── terraform │ ├── README.md │ ├── controls │ │ ├── config.yaml │ │ └── validate.rb │ ├── inspec.yml │ └── libraries │ │ └── terraform_syntax.rb ├── vault.io │ ├── README.md │ ├── controls │ │ ├── config.yaml │ │ └── validate.rb │ └── inspec.yml └── vault │ ├── README.md │ ├── controls │ ├── config.yaml │ └── validate.rb │ └── inspec.yml ├── run.sh └── target ├── Dockerfile ├── Makefile ├── README.md ├── background.sh ├── future └── Makefile └── interactive.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # This container is essentially a stack with inspec and kramdown. 5 | # See Gemfile for gems and software versions used with this container. 6 | FROM docker.mirror.hashicorp.services/ruby:2.5.1 7 | 8 | WORKDIR / 9 | 10 | COPY Gemfile / 11 | RUN bundle install 12 | 13 | COPY . / 14 | 15 | ENV CHEF_LICENSE accept-silent 16 | 17 | RUN for profile in ../profiles/*/ ; \ 18 | do bundle exec inspec vendor --overwrite $profile ; \ 19 | done 20 | 21 | ENTRYPOINT ["bundle", "exec", "inspec"] 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | source 'https://rubygems.org' do 5 | gem 'inspec', '4.18.100' 6 | gem 'inspec-bin', '4.18.100' 7 | gem 'kramdown', '2.3.1' 8 | gem 'kramdown-parser-gfm', '1.1.0' 9 | gem 'octokit', '4.22.0' 10 | end 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 HashiCorp, Inc. 2 | 3 | Mozilla Public License Version 2.0 4 | ================================== 5 | 6 | 1. Definitions 7 | -------------- 8 | 9 | 1.1. "Contributor" 10 | means each individual or legal entity that creates, contributes to 11 | the creation of, or owns Covered Software. 12 | 13 | 1.2. "Contributor Version" 14 | means the combination of the Contributions of others (if any) used 15 | by a Contributor and that particular Contributor's Contribution. 16 | 17 | 1.3. "Contribution" 18 | means Covered Software of a particular Contributor. 19 | 20 | 1.4. "Covered Software" 21 | means Source Code Form to which the initial Contributor has attached 22 | the notice in Exhibit A, the Executable Form of such Source Code 23 | Form, and Modifications of such Source Code Form, in each case 24 | including portions thereof. 25 | 26 | 1.5. "Incompatible With Secondary Licenses" 27 | means 28 | 29 | (a) that the initial Contributor has attached the notice described 30 | in Exhibit B to the Covered Software; or 31 | 32 | (b) that the Covered Software was made available under the terms of 33 | version 1.1 or earlier of the License, but not also under the 34 | terms of a Secondary License. 35 | 36 | 1.6. "Executable Form" 37 | means any form of the work other than Source Code Form. 38 | 39 | 1.7. "Larger Work" 40 | means a work that combines Covered Software with other material, in 41 | a separate file or files, that is not Covered Software. 42 | 43 | 1.8. "License" 44 | means this document. 45 | 46 | 1.9. "Licensable" 47 | means having the right to grant, to the maximum extent possible, 48 | whether at the time of the initial grant or subsequently, any and 49 | all of the rights conveyed by this License. 50 | 51 | 1.10. "Modifications" 52 | means any of the following: 53 | 54 | (a) any file in Source Code Form that results from an addition to, 55 | deletion from, or modification of the contents of Covered 56 | Software; or 57 | 58 | (b) any new file in Source Code Form that contains any Covered 59 | Software. 60 | 61 | 1.11. "Patent Claims" of a Contributor 62 | means any patent claim(s), including without limitation, method, 63 | process, and apparatus claims, in any patent Licensable by such 64 | Contributor that would be infringed, but for the grant of the 65 | License, by the making, using, selling, offering for sale, having 66 | made, import, or transfer of either its Contributions or its 67 | Contributor Version. 68 | 69 | 1.12. "Secondary License" 70 | means either the GNU General Public License, Version 2.0, the GNU 71 | Lesser General Public License, Version 2.1, the GNU Affero General 72 | Public License, Version 3.0, or any later versions of those 73 | licenses. 74 | 75 | 1.13. "Source Code Form" 76 | means the form of the work preferred for making modifications. 77 | 78 | 1.14. "You" (or "Your") 79 | means an individual or a legal entity exercising rights under this 80 | License. For legal entities, "You" includes any entity that 81 | controls, is controlled by, or is under common control with You. For 82 | purposes of this definition, "control" means (a) the power, direct 83 | or indirect, to cause the direction or management of such entity, 84 | whether by contract or otherwise, or (b) ownership of more than 85 | fifty percent (50%) of the outstanding shares or beneficial 86 | ownership of such entity. 87 | 88 | 2. License Grants and Conditions 89 | -------------------------------- 90 | 91 | 2.1. Grants 92 | 93 | Each Contributor hereby grants You a world-wide, royalty-free, 94 | non-exclusive license: 95 | 96 | (a) under intellectual property rights (other than patent or trademark) 97 | Licensable by such Contributor to use, reproduce, make available, 98 | modify, display, perform, distribute, and otherwise exploit its 99 | Contributions, either on an unmodified basis, with Modifications, or 100 | as part of a Larger Work; and 101 | 102 | (b) under Patent Claims of such Contributor to make, use, sell, offer 103 | for sale, have made, import, and otherwise transfer either its 104 | Contributions or its Contributor Version. 105 | 106 | 2.2. Effective Date 107 | 108 | The licenses granted in Section 2.1 with respect to any Contribution 109 | become effective for each Contribution on the date the Contributor first 110 | distributes such Contribution. 111 | 112 | 2.3. Limitations on Grant Scope 113 | 114 | The licenses granted in this Section 2 are the only rights granted under 115 | this License. No additional rights or licenses will be implied from the 116 | distribution or licensing of Covered Software under this License. 117 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 118 | Contributor: 119 | 120 | (a) for any code that a Contributor has removed from Covered Software; 121 | or 122 | 123 | (b) for infringements caused by: (i) Your and any other third party's 124 | modifications of Covered Software, or (ii) the combination of its 125 | Contributions with other software (except as part of its Contributor 126 | Version); or 127 | 128 | (c) under Patent Claims infringed by Covered Software in the absence of 129 | its Contributions. 130 | 131 | This License does not grant any rights in the trademarks, service marks, 132 | or logos of any Contributor (except as may be necessary to comply with 133 | the notice requirements in Section 3.4). 134 | 135 | 2.4. Subsequent Licenses 136 | 137 | No Contributor makes additional grants as a result of Your choice to 138 | distribute the Covered Software under a subsequent version of this 139 | License (see Section 10.2) or under the terms of a Secondary License (if 140 | permitted under the terms of Section 3.3). 141 | 142 | 2.5. Representation 143 | 144 | Each Contributor represents that the Contributor believes its 145 | Contributions are its original creation(s) or it has sufficient rights 146 | to grant the rights to its Contributions conveyed by this License. 147 | 148 | 2.6. Fair Use 149 | 150 | This License is not intended to limit any rights You have under 151 | applicable copyright doctrines of fair use, fair dealing, or other 152 | equivalents. 153 | 154 | 2.7. Conditions 155 | 156 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 157 | in Section 2.1. 158 | 159 | 3. Responsibilities 160 | ------------------- 161 | 162 | 3.1. Distribution of Source Form 163 | 164 | All distribution of Covered Software in Source Code Form, including any 165 | Modifications that You create or to which You contribute, must be under 166 | the terms of this License. You must inform recipients that the Source 167 | Code Form of the Covered Software is governed by the terms of this 168 | License, and how they can obtain a copy of this License. You may not 169 | attempt to alter or restrict the recipients' rights in the Source Code 170 | Form. 171 | 172 | 3.2. Distribution of Executable Form 173 | 174 | If You distribute Covered Software in Executable Form then: 175 | 176 | (a) such Covered Software must also be made available in Source Code 177 | Form, as described in Section 3.1, and You must inform recipients of 178 | the Executable Form how they can obtain a copy of such Source Code 179 | Form by reasonable means in a timely manner, at a charge no more 180 | than the cost of distribution to the recipient; and 181 | 182 | (b) You may distribute such Executable Form under the terms of this 183 | License, or sublicense it under different terms, provided that the 184 | license for the Executable Form does not attempt to limit or alter 185 | the recipients' rights in the Source Code Form under this License. 186 | 187 | 3.3. Distribution of a Larger Work 188 | 189 | You may create and distribute a Larger Work under terms of Your choice, 190 | provided that You also comply with the requirements of this License for 191 | the Covered Software. If the Larger Work is a combination of Covered 192 | Software with a work governed by one or more Secondary Licenses, and the 193 | Covered Software is not Incompatible With Secondary Licenses, this 194 | License permits You to additionally distribute such Covered Software 195 | under the terms of such Secondary License(s), so that the recipient of 196 | the Larger Work may, at their option, further distribute the Covered 197 | Software under the terms of either this License or such Secondary 198 | License(s). 199 | 200 | 3.4. Notices 201 | 202 | You may not remove or alter the substance of any license notices 203 | (including copyright notices, patent notices, disclaimers of warranty, 204 | or limitations of liability) contained within the Source Code Form of 205 | the Covered Software, except that You may alter any license notices to 206 | the extent required to remedy known factual inaccuracies. 207 | 208 | 3.5. Application of Additional Terms 209 | 210 | You may choose to offer, and to charge a fee for, warranty, support, 211 | indemnity or liability obligations to one or more recipients of Covered 212 | Software. However, You may do so only on Your own behalf, and not on 213 | behalf of any Contributor. You must make it absolutely clear that any 214 | such warranty, support, indemnity, or liability obligation is offered by 215 | You alone, and You hereby agree to indemnify every Contributor for any 216 | liability incurred by such Contributor as a result of warranty, support, 217 | indemnity or liability terms You offer. You may include additional 218 | disclaimers of warranty and limitations of liability specific to any 219 | jurisdiction. 220 | 221 | 4. Inability to Comply Due to Statute or Regulation 222 | --------------------------------------------------- 223 | 224 | If it is impossible for You to comply with any of the terms of this 225 | License with respect to some or all of the Covered Software due to 226 | statute, judicial order, or regulation then You must: (a) comply with 227 | the terms of this License to the maximum extent possible; and (b) 228 | describe the limitations and the code they affect. Such description must 229 | be placed in a text file included with all distributions of the Covered 230 | Software under this License. Except to the extent prohibited by statute 231 | or regulation, such description must be sufficiently detailed for a 232 | recipient of ordinary skill to be able to understand it. 233 | 234 | 5. Termination 235 | -------------- 236 | 237 | 5.1. The rights granted under this License will terminate automatically 238 | if You fail to comply with any of its terms. However, if You become 239 | compliant, then the rights granted under this License from a particular 240 | Contributor are reinstated (a) provisionally, unless and until such 241 | Contributor explicitly and finally terminates Your grants, and (b) on an 242 | ongoing basis, if such Contributor fails to notify You of the 243 | non-compliance by some reasonable means prior to 60 days after You have 244 | come back into compliance. Moreover, Your grants from a particular 245 | Contributor are reinstated on an ongoing basis if such Contributor 246 | notifies You of the non-compliance by some reasonable means, this is the 247 | first time You have received notice of non-compliance with this License 248 | from such Contributor, and You become compliant prior to 30 days after 249 | Your receipt of the notice. 250 | 251 | 5.2. If You initiate litigation against any entity by asserting a patent 252 | infringement claim (excluding declaratory judgment actions, 253 | counter-claims, and cross-claims) alleging that a Contributor Version 254 | directly or indirectly infringes any patent, then the rights granted to 255 | You by any and all Contributors for the Covered Software under Section 256 | 2.1 of this License shall terminate. 257 | 258 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 259 | end user license agreements (excluding distributors and resellers) which 260 | have been validly granted by You or Your distributors under this License 261 | prior to termination shall survive termination. 262 | 263 | ************************************************************************ 264 | * * 265 | * 6. Disclaimer of Warranty * 266 | * ------------------------- * 267 | * * 268 | * Covered Software is provided under this License on an "as is" * 269 | * basis, without warranty of any kind, either expressed, implied, or * 270 | * statutory, including, without limitation, warranties that the * 271 | * Covered Software is free of defects, merchantable, fit for a * 272 | * particular purpose or non-infringing. The entire risk as to the * 273 | * quality and performance of the Covered Software is with You. * 274 | * Should any Covered Software prove defective in any respect, You * 275 | * (not any Contributor) assume the cost of any necessary servicing, * 276 | * repair, or correction. This disclaimer of warranty constitutes an * 277 | * essential part of this License. No use of any Covered Software is * 278 | * authorized under this License except under this disclaimer. * 279 | * * 280 | ************************************************************************ 281 | 282 | ************************************************************************ 283 | * * 284 | * 7. Limitation of Liability * 285 | * -------------------------- * 286 | * * 287 | * Under no circumstances and under no legal theory, whether tort * 288 | * (including negligence), contract, or otherwise, shall any * 289 | * Contributor, or anyone who distributes Covered Software as * 290 | * permitted above, be liable to You for any direct, indirect, * 291 | * special, incidental, or consequential damages of any character * 292 | * including, without limitation, damages for lost profits, loss of * 293 | * goodwill, work stoppage, computer failure or malfunction, or any * 294 | * and all other commercial damages or losses, even if such party * 295 | * shall have been informed of the possibility of such damages. This * 296 | * limitation of liability shall not apply to liability for death or * 297 | * personal injury resulting from such party's negligence to the * 298 | * extent applicable law prohibits such limitation. Some * 299 | * jurisdictions do not allow the exclusion or limitation of * 300 | * incidental or consequential damages, so this exclusion and * 301 | * limitation may not apply to You. * 302 | * * 303 | ************************************************************************ 304 | 305 | 8. Litigation 306 | ------------- 307 | 308 | Any litigation relating to this License may be brought only in the 309 | courts of a jurisdiction where the defendant maintains its principal 310 | place of business and such litigation shall be governed by laws of that 311 | jurisdiction, without reference to its conflict-of-law provisions. 312 | Nothing in this Section shall prevent a party's ability to bring 313 | cross-claims or counter-claims. 314 | 315 | 9. Miscellaneous 316 | ---------------- 317 | 318 | This License represents the complete agreement concerning the subject 319 | matter hereof. If any provision of this License is held to be 320 | unenforceable, such provision shall be reformed only to the extent 321 | necessary to make it enforceable. Any law or regulation which provides 322 | that the language of a contract shall be construed against the drafter 323 | shall not be used to construe this License against a Contributor. 324 | 325 | 10. Versions of the License 326 | --------------------------- 327 | 328 | 10.1. New Versions 329 | 330 | Mozilla Foundation is the license steward. Except as provided in Section 331 | 10.3, no one other than the license steward has the right to modify or 332 | publish new versions of this License. Each version will be given a 333 | distinguishing version number. 334 | 335 | 10.2. Effect of New Versions 336 | 337 | You may distribute the Covered Software under the terms of the version 338 | of the License under which You originally received the Covered Software, 339 | or under the terms of any subsequent version published by the license 340 | steward. 341 | 342 | 10.3. Modified Versions 343 | 344 | If you create software not governed by this License, and you want to 345 | create a new license for such software, you may create and use a 346 | modified version of this License if you rename the license and remove 347 | any references to the name of the license steward (except to note that 348 | such modified license differs from this License). 349 | 350 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 351 | Licenses 352 | 353 | If You choose to distribute Source Code Form that is Incompatible With 354 | Secondary Licenses under the terms of this version of the License, the 355 | notice described in Exhibit B of this License must be attached. 356 | 357 | Exhibit A - Source Code Form License Notice 358 | ------------------------------------------- 359 | 360 | This Source Code Form is subject to the terms of the Mozilla Public 361 | License, v. 2.0. If a copy of the MPL was not distributed with this 362 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 363 | 364 | If it is not possible or desirable to put the notice in a particular 365 | file, then You may include the notice in a location (such as a LICENSE 366 | file in a relevant directory) where a recipient would be likely to look 367 | for such a notice. 368 | 369 | You may add additional accurate notices of copyright ownership. 370 | 371 | Exhibit B - "Incompatible With Secondary Licenses" Notice 372 | --------------------------------------------------------- 373 | 374 | This Source Code Form is "Incompatible With Secondary Licenses", as 375 | defined by the Mozilla Public License, v. 2.0. 376 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_NAME=inspec 2 | DOCKER_USERNAME=hashieducation 3 | 4 | define GetImageID 5 | $$(docker images --filter=reference=$(1) --format "{{.ID}}" | awk '{getline;print}') 6 | endef 7 | 8 | # Otherwise we just use what the user gave us 9 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD) 10 | ifeq ($(BRANCH),master) 11 | DOCKER_TAG += "master" 12 | else 13 | DOCKER_TAG += $(BRANCH) 14 | endif 15 | 16 | build: 17 | @echo "==> Building Docker image..." 18 | @docker build \ 19 | --rm \ 20 | -t \ 21 | $(IMAGE_NAME):$(DOCKER_TAG) \ 22 | . 23 | push: build 24 | # open onepassword://search/ej6uodvxxvcetihbgwdy2wnoh4 25 | # export DOCKER_API_TOKEN= 26 | @echo $(DOCKER_API_TOKEN) | \ 27 | docker login --username $(DOCKER_USERNAME) --password-stdin 28 | @docker tag $(call GetImageID,$(IMAGE_NAME):$(DOCKER_TAG)) \ 29 | $(DOCKER_USERNAME)/$(IMAGE_NAME):$(DOCKER_TAG) 30 | @docker push \ 31 | $(DOCKER_USERNAME)/$(IMAGE_NAME):$(DOCKER_TAG) 32 | 33 | .DEFAULT_GOAL := build 34 | 35 | .PHONY: build push 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 |

4 | 5 |

6 | 7 | This repo contains [inspec](https://www.inspec.io/) integration with the [learn](https://github.com/hashicorp/learn) platform. It uses docker to run inspec. Tests/Controls are __automatically generated__ by extracting fenced code blocks from the markdown (mdx). Each test then runs against an target docker container via a mounted docker socket. You can customize the environment of this "target" with real world environmental variables such as AWS Keys to do live tests with example code. You can modify this target with stand-in configurations by rebuilding the target docker container. 8 | 9 | > Currently these tests run syntax checks against terraform hcl, json and shell. 10 | > See the [RFC](https://docs.google.com/document/d/1TgyrGkmdr4FCyLHN9OKYR2bEMNlJIFNS8QhQyTBXDlg/edit#) for an explanation of active vs passive testing. 11 | 12 | # Usage 13 | 14 | Executing this code requires two containers. The `inspec` container is not actually required but is provided to minimize workstation requirements via [`run.sh`](run.sh). The `inspec-target` is automatically spun up. You can also run it interactivly to debug using [`./target/interactive.sh`](target/interactive.sh) 15 | 16 | ## Requirements 17 | 18 | Docker is required, you can download it [here](https://hub.docker.com/editions/community/docker-ce-desktop-mac). 19 | 20 | ## Executing an inspec profile 21 | 22 | In a terminal window , run the `./run.sh` script shown below. The code extracts markdown content from your local checkout/branch of the learn repo. You must provide the path to the root of your local learn repo with `-d`. You can then pass which product you wish to run tests against with. These product names correspond to inspec [profiles](https://www.inspec.io/docs/reference/profiles/) 23 | 24 | 25 | ```shell 26 | # Build the inspect container 27 | cd $(git rev-parse --show-cdup) 28 | make 29 | # Build the inspec target container 30 | cd target 31 | make 32 | ./run.sh -p terraform -d ~/src/learn 33 | ./run.sh -p vault -d ~/src/learn 34 | ./run.sh -p nomad -d ~/src/learn 35 | ./run.sh -p consul -d ~/src/learn 36 | ``` 37 | 38 | > You can run all profiles with `-p all` 39 | > You can pipe the output with color with `| less -r` 40 | ### Product profiles 41 | 42 | 43 | | Profile | Notes | 44 | | ---------------------------------- | -----------------------------------------------------------------------------:| 45 | | [terraform](profiles/terraform) | Extracts all `hcl`, `shell`, `json` and `yaml` codeblocks and validates them | 46 | | [vault](profiles/vault) | Extracts all `shell`, `json` and `yaml` codeblocks validates them | 47 | | [nomad](profiles/nomad) | Extracts all `shell`, `json` and `yaml` codeblocks validates them | 48 | | [consul](profiles/consul) | Extracts all `shell`, `json` and `yaml` codeblocks validates them | 49 | 50 | > `terraform` validates syntax by passing each block as stdin via `terraform fmt -`. 51 | 52 | ### Utility profiles 53 | 54 | | Profile | Notes | 55 | | ------------------------ | --------------------------------------------------------------------------------------------------------:| 56 | | [all](profiles/all) | For use with the `./run.sh` script. Runs all product profiles | 57 | | [shared](profiles/shared) | Used to store shared custom resources for inspec [libraries](profiles/shared/libraries) | 58 | | [github](profiles/github) | Used with Github Action, expects `GITHUB` environment vars for commit lookup | 59 | 60 | # Support Files 61 | 62 | ## [`run.sh`](run.sh) 63 | 64 | This script is used by authors and developers to run the tests locally. 65 | 66 | ## [`input.yml`](input.yml) 67 | 68 | This file contains inputs to (globally) to the inspec profiles. It currently is used by `shell_syntax` custom resource to do dynamic replacments for placeholders in the code. This replacements hash allows us to run syntax checks on commands that otherwise would be invalid syntax with the placeholder. 69 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | # This is the Github action code used for CI runs 6 | # The input system will in the future allow more then learn 7 | # to be targeted by the profiles in this directory. 8 | name: 'Check markdown codeblocks' 9 | author: 'Zack Smith' 10 | description: 'Run inspec against markdown' 11 | inputs: 12 | profile: 13 | description: 'The profile to run' 14 | required: false 15 | default: 'all' 16 | markdown: 17 | description: 'The directory containing the markdown' 18 | required: true 19 | github_token: 20 | description: 'Github token used by github profile' 21 | required: true 22 | file_pattern: 23 | description: 'The file glob e.g. pages/**/*.mdx' 24 | required: true 25 | default_branch: 26 | description: 'The default branch in the target repository' 27 | default: 'master' 28 | required: false 29 | outputs: 30 | inspec-output: 31 | description: 'The output of inspec' 32 | runs: 33 | using: 'docker' 34 | image: 'docker://docker.mirror.hashicorp.services/hashieducation/inspec:master' 35 | env: 36 | MARKDOWN: ${{ inputs.markdown }} 37 | GITHUB_TOKEN: ${{ inputs.github_token }} 38 | FILE_PATTERN: ${{ inputs.file_pattern }} 39 | DEFAULT_BRANCH: ${{ inputs.default_branch }} 40 | args: 41 | - 'exec' 42 | - /profiles/${{ inputs.profile }} 43 | - '--target=docker://inspec-target' 44 | - '--reporter' 45 | - 'cli' 46 | - 'html:inspec.html' 47 | - '--show-progress' 48 | - '--no-color' 49 | - '--no-distinct-exit' 50 | - '--input-file' 51 | - '/input.yml' 52 | branding: 53 | color: 'black' 54 | icon: 'dollar-sign' 55 | -------------------------------------------------------------------------------- /images/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/learn-inspec/1957d8302ab51062ad1b42493223aeed1356a20b/images/diagram.png -------------------------------------------------------------------------------- /input.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | # Pesudo variables to be replaced 6 | # This allows better test coverage 7 | replacements: 8 | '': 's.KkNJYWF5g0pomcCLEmDdOVCW' 9 | '': '10.1.2.3' 10 | '': '10.1.2.4' 11 | '': '10.1.2.5' 12 | '' : 'YWxhZGRpbjpvcGVuc2VzYW1l' 13 | '' : 'https://www.consul.io' 14 | '' : '06f0a6e4-ce89-b7a4-8429-4d5f987ff3c1' 15 | '' : 'resource_group' 16 | '' : 'learnlab' 17 | '' : 'consul-learn-test' 18 | '' : 'learn-hcs-aks-client' 19 | -------------------------------------------------------------------------------- /profiles/README.md: -------------------------------------------------------------------------------- 1 | # Inspec profiles 2 | 3 | This directory contains the inspec profiles. 4 | 5 | # Testing the .io sites 6 | 7 | The .io profiles are used to test the respective products io site. Pass the respective products repo to the `-d` flag and not the learn repo. 8 | -------------------------------------------------------------------------------- /profiles/all/.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /profiles/all/README.md: -------------------------------------------------------------------------------- 1 | # Run all tests 2 | 3 | Runs all learn profiles. 4 | 5 | ## Usage 6 | 7 | ```shell 8 | ./run.sh -d ~/src/learn -p all 9 | ``` 10 | -------------------------------------------------------------------------------- /profiles/all/controls/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/learn-inspec/1957d8302ab51062ad1b42493223aeed1356a20b/profiles/all/controls/.gitkeep -------------------------------------------------------------------------------- /profiles/all/controls/all.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | include_controls 'vault' 5 | include_controls 'terraform' 6 | include_controls 'consul' 7 | include_controls 'nomad' 8 | -------------------------------------------------------------------------------- /profiles/all/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: all 3 | title: Test all of learn profiles 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This runs all profiles 9 | version: 1.0.0 10 | depends: 11 | - name: vault 12 | path: ../vault 13 | - name: terraform 14 | path: ../terraform 15 | - name: nomad 16 | path: ../nomad 17 | - name: consul 18 | path: ../consul 19 | inputs: 20 | - name: files_to_check 21 | type: array 22 | value: [] 23 | -------------------------------------------------------------------------------- /profiles/consul/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```shell 4 | ./run.sh -p consul -d ~/src/learn 5 | ``` 6 | -------------------------------------------------------------------------------- /profiles/consul/controls/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | markdown_glob: 'content/tutorials/consul/**/*.mdx' 6 | -------------------------------------------------------------------------------- /profiles/consul/controls/validate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | require 'getoptlong' 6 | require 'kramdown' 7 | require 'yaml' 8 | 9 | $config = YAML.load(File.read("#{__dir__}/config.yaml")) 10 | 11 | markdown_files = if input('files_to_check').empty? 12 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") 13 | else 14 | # This will return the intersection of the two arrays 15 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") && input('files_to_check') 16 | end 17 | 18 | raise "No markdown files found!}" if markdown_files.count.zero? 19 | 20 | include_controls "shared" 21 | 22 | # TODO: Can't use this as inspec.command doesn't work as inspec in nil 23 | # See if statement below, should track down why this doesn't work 24 | # require_resource(profile: 'terraform', 25 | # resource: 'terraform_syntax', 26 | # as: 'hcl_syntax') 27 | 28 | 29 | # Enumerate our markdown files 30 | markdown_files.each do |file| 31 | # Load the front matter (no parsing needed) 32 | front_matter = YAML.load_file(file) 33 | 34 | control file do 35 | impact 1.0 36 | title front_matter['name'] 37 | desc front_matter['description'] 38 | 39 | ref File.basename(file), 40 | url: 'https://github.com/hashicorp/learn/blob/master/#{file.split("/").drop(1).join("/")}' 41 | 42 | # Parse the markdown 43 | markdown = Kramdown::Document.new(File.read(file), input: 'GFM') 44 | 45 | markdown.root.children.each_with_index do |section,index| 46 | case section.type 47 | # Parse the codeblocks 48 | when :codeblock 49 | case section.options[:lang] 50 | when 'json' 51 | describe json_syntax(value: section.value) do 52 | it { should be_valid } 53 | end 54 | when /shell|shell-session/ 55 | describe shell_syntax(value: section.value, replacements: input("replacements") ) do 56 | it { should be_valid } 57 | end 58 | when 'yaml' 59 | describe yaml_syntax(value: section.value) do 60 | it { should be_valid } 61 | end 62 | when 'hcl' 63 | # I should be able to use require_resource above but it doesn't work 64 | if input('products_used').include?("terraform") 65 | describe terraform_syntax(hcl: section.value) do 66 | it { should be_valid } 67 | end 68 | else 69 | describe hcl_syntax(hcl: section.value) do 70 | it { should be_valid } 71 | end 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /profiles/consul/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: consul 3 | title: Consul profile 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This profile runs syntax checks for markdown in the consul directory 9 | version: 2.0.0 10 | depends: 11 | - name: shared 12 | path: ../shared 13 | inputs: 14 | - name: replacements 15 | type: hash 16 | value: 17 | '': '10.1.2.3' 18 | '': '10.1.2.4' 19 | '': '10.1.2.5' 20 | '' : 'YWxhZGRpbjpvcGVuc2VzYW1l' 21 | - name: products_used 22 | type: array 23 | value: ['Consul','Consul Enterprise'] 24 | - name: files_to_check 25 | value: [] 26 | -------------------------------------------------------------------------------- /profiles/github/.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /profiles/github/README.md: -------------------------------------------------------------------------------- 1 | # Github Action Profile 2 | 3 | This profile checks all markdown in the path (with glob). 4 | 5 | ## Example Workflow 6 | 7 | ```yaml 8 | name: HashiCorp Syntax Checker 9 | on: 10 | push: 11 | paths: 12 | - "pages/**/*.mdx" 13 | - "shared/**/*.mdx" 14 | jobs: 15 | inspec: 16 | name: Test Markdown Code Blocks 17 | runs-on: ubuntu-latest 18 | services: 19 | inspec-target: 20 | image: hashieducation/inspec-target 21 | options: --name inspec-target 22 | steps: 23 | - name: Download Markdown 24 | uses: actions/checkout@v2.0.0 25 | - name: Run Syntax Checks 26 | uses: hashicorp/learn-inspec@master 27 | with: 28 | profile: 'github' 29 | markdown: ${{github.workspace}} 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | file_pattern: "**/*.mdx" 32 | - name: Upload Test Results 33 | uses: actions/upload-artifact@v1 34 | if: always() 35 | with: 36 | name: HTML Report 37 | path: inspec.html 38 | ``` 39 | 40 | This workflow automatically tests all markdown with the path. 41 | -------------------------------------------------------------------------------- /profiles/github/controls/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/learn-inspec/1957d8302ab51062ad1b42493223aeed1356a20b/profiles/github/controls/.gitkeep -------------------------------------------------------------------------------- /profiles/github/controls/action.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # Sanity check 5 | require 'octokit' 6 | require 'cgi' 7 | require 'getoptlong' 8 | require 'kramdown' 9 | require 'yaml' 10 | 11 | if ENV['GITHUB_ACTIONS'] != 'true' 12 | raise "This profile is only meant to be used with Github Actions" 13 | end 14 | 15 | # TODO: Need to clean this up a bit 16 | # Use the github api to find the files changed between the two commits 17 | # The token here is set by actions via secrets.GITHUB_TOKEN for the run. 18 | # The event json doesn't have this data so I implement it here. 19 | github = Octokit::Client.new(:access_token => ENV['GITHUB_TOKEN']) 20 | puts "Running under Github Actions" 21 | puts "\tUsing default branch: #{ENV['DEFAULT_BRANCH']}" 22 | 23 | repository = ENV['GITHUB_REPOSITORY'] 24 | branch = CGI.escape(ENV['GITHUB_REF'].sub('refs/heads/','')) 25 | 26 | begin 27 | feature_branch = github.ref(repository, 28 | "heads/#{branch}") 29 | 30 | master_branch = github.ref(repository, 31 | "heads/#{ENV['DEFAULT_BRANCH']}") 32 | comparison = github.compare(repository, 33 | master_branch.object.sha, 34 | feature_branch.object.sha) 35 | 36 | # Sanity check 37 | if comparison.status == "identical" 38 | puts "Commits are identical (merge commit?)" 39 | skip_control 'all' 40 | end 41 | 42 | # This filters our tests just down to the context of the PR's branch 43 | # TODO: should I use default_branch here from the API instead of master 44 | files_to_check = comparison.files.select do |file| 45 | file.status == 'added' or file.status == 'modified' 46 | end.map{|file| "#{ENV['MARKDOWN']}/#{file.filename}"} 47 | 48 | rescue Octokit::NotFound => e 49 | puts "Branch #{ENV['GITHUB_REF']} no longer exists" 50 | puts "Unable to lookup files that changed: #{e.inspect}" 51 | skip_control 'all' 52 | files_to_check = [] 53 | end 54 | 55 | # This syntax means an intersection of the two arrays. 56 | # Functionaly it means, file those files that have been edited and match our glob 57 | markdown_files = Dir.glob("#{ENV['MARKDOWN']}/#{ENV['FILE_PATTERN']}") & files_to_check 58 | 59 | # Include our shared resources 60 | include_controls "shared" 61 | 62 | # Enumerate our matching markdown files 63 | markdown_files.each do |file| 64 | # Load the front matter (no parsing needed) 65 | front_matter = YAML.load_file(file) 66 | 67 | control file do 68 | impact 1.0 69 | title front_matter['name'] 70 | desc front_matter['description'] 71 | 72 | ref File.basename(file), 73 | url: 'https://github.com/hashicorp/learn/blob/master/#{file.split("/").drop(1).join("/")}' 74 | 75 | # Parse the markdown 76 | markdown = Kramdown::Document.new(File.read(file), input: 'GFM') 77 | 78 | markdown.root.children.each_with_index do |section,index| 79 | case section.type 80 | # Loop through each type of code block 81 | when :codeblock 82 | case section.options[:lang] 83 | when 'json' 84 | describe json_syntax(value: section.value) do 85 | it { should be_valid } 86 | end 87 | when /^shell|shell-session/ 88 | describe shell_syntax(value: section.value, replacements: input("replacements") ) do 89 | it { should be_valid } 90 | end 91 | when 'yaml' 92 | describe yaml_syntax(value: section.value) do 93 | it { should be_valid } 94 | end 95 | when 'hcl' 96 | # Use terraform to verify hcl for terraform products 97 | # TODO: Figure out why require_resource with an override causes 98 | # inspec.command not to work ( be nil ) 99 | if front_matter['products_used'].include?('terraform') 100 | describe terraform_syntax(hcl: section.value) do 101 | it { should be_valid } 102 | end 103 | # TODO: Update this when https://github.com/hashicorp/packer/pull/10500 is merged 104 | elsif front_matter['products_used'].include?('packer') 105 | describe terraform_syntax(hcl: section.value) do 106 | it { should be_valid } 107 | end 108 | else 109 | describe hcl_syntax(hcl: section.value) do 110 | it { should be_valid } 111 | end 112 | end 113 | end 114 | end 115 | end 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /profiles/github/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github 3 | title: Github Actions Profile 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This profile dynamically loads controls based on commit changes 9 | version: 2.0.6 10 | depends: 11 | - name: all 12 | path: ../all 13 | inputs: 14 | - name: replacements 15 | type: hash 16 | value: 17 | '': '10.1.2.3' 18 | '': '10.1.2.4' 19 | '': '10.1.2.5' 20 | '' : 'YWxhZGRpbjpvcGVuc2VzYW1l' 21 | -------------------------------------------------------------------------------- /profiles/nomad.io/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```shell 4 | mkdir -p ~/src 5 | git clone git@github.com:hashicorp/nomad.git ~/src/nomad 6 | 7 | ``` 8 | 9 | ```shell 10 | ./run.sh -p nomad.io -d ~/src/nomad 11 | ``` 12 | -------------------------------------------------------------------------------- /profiles/nomad.io/controls/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | markdown_glob: 'website/pages/docs/**/*.mdx' 6 | -------------------------------------------------------------------------------- /profiles/nomad.io/controls/validate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | require 'getoptlong' 6 | require 'kramdown' 7 | require 'yaml' 8 | 9 | $config = YAML.load(File.read("#{__dir__}/config.yaml")) 10 | 11 | markdown_files = Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") 12 | 13 | raise "No markdown files found!}" if markdown_files.count.zero? 14 | 15 | include_controls "shared" 16 | 17 | 18 | # Enumerate our markdown files 19 | markdown_files.each do |file| 20 | # Load the front matter (no parsing needed) 21 | front_matter = YAML.load_file(file) 22 | 23 | control file do 24 | impact 1.0 25 | title front_matter['page_title'] 26 | desc front_matter['description'] 27 | 28 | ref File.basename(file), 29 | url: 'https://github.com/hashicorp/learn/blob/master/#{file.split("/").drop(1).join("/")}' 30 | 31 | 32 | # Parse the markdown 33 | markdown = Kramdown::Document.new(File.read(file), input: 'GFM') 34 | 35 | markdown.root.children.each_with_index do |section,index| 36 | case section.type 37 | # Parse the codeblocks 38 | when :codeblock 39 | case section.options[:lang] 40 | when 'json' 41 | describe json_syntax(value: section.value) do 42 | it { should be_valid } 43 | end 44 | when 'shell' 45 | describe shell_syntax(value: section.value, replacements: input("replacements") ) do 46 | it { should be_valid } 47 | end 48 | when 'yaml' 49 | describe yaml_syntax(value: section.value) do 50 | it { should be_valid } 51 | end 52 | when 'hcl' 53 | describe hcl_syntax(hcl: section.value) do 54 | it { should be_valid } 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /profiles/nomad.io/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: nomad.io 3 | title: Nomad.io profile 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This profile checks the syntax of the https://nomad.io website 9 | version: 2.0.0 10 | depends: 11 | - name: shared 12 | path: ../shared 13 | inputs: 14 | - name: replacements 15 | type: hash 16 | value: 17 | '': 's.KkNJYWF5g0pomcCLEmDdOVCW' 18 | - name: products_used 19 | type: array 20 | value: ['Nomad','Nomad Enterprise'] 21 | - name: files_to_check 22 | value: [] 23 | -------------------------------------------------------------------------------- /profiles/nomad/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```shell 4 | ./run.sh -p nomad -d ~/src/learn 5 | ``` 6 | -------------------------------------------------------------------------------- /profiles/nomad/controls/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | markdown_glob: 'content/tutorials/nomad/**/*.mdx' 6 | -------------------------------------------------------------------------------- /profiles/nomad/controls/validate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | require 'getoptlong' 6 | require 'kramdown' 7 | require 'yaml' 8 | 9 | $config = YAML.load(File.read("#{__dir__}/config.yaml")) 10 | 11 | markdown_files = if input('files_to_check').empty? 12 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") 13 | else 14 | # This will return the intersection of the two arrays 15 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") && input('files_to_check') 16 | end 17 | 18 | raise "No markdown files found!}" if markdown_files.count.zero? 19 | 20 | include_controls "shared" 21 | 22 | # TODO: Can't use this as inspec.command doesn't work as inspec in nil 23 | # See if statement below, should track down why this doesn't work 24 | # require_resource(profile: 'terraform', 25 | # resource: 'terraform_syntax', 26 | # as: 'hcl_syntax') 27 | 28 | 29 | # Enumerate our markdown files 30 | markdown_files.each do |file| 31 | # Load the front matter (no parsing needed) 32 | front_matter = YAML.load_file(file) 33 | 34 | control file do 35 | impact 1.0 36 | title front_matter['name'] 37 | desc front_matter['description'] 38 | 39 | ref File.basename(file), 40 | url: 'https://github.com/hashicorp/learn/blob/master/#{file.split("/").drop(1).join("/")}' 41 | 42 | # Parse the markdown 43 | markdown = Kramdown::Document.new(File.read(file), input: 'GFM') 44 | 45 | markdown.root.children.each_with_index do |section,index| 46 | case section.type 47 | # Parse the codeblocks 48 | when :codeblock 49 | case section.options[:lang] 50 | when 'json' 51 | describe json_syntax(value: section.value) do 52 | it { should be_valid } 53 | end 54 | when /shell|shell-session/ 55 | describe shell_syntax(value: section.value, replacements: input("replacements") ) do 56 | it { should be_valid } 57 | end 58 | when 'yaml' 59 | describe yaml_syntax(value: section.value) do 60 | it { should be_valid } 61 | end 62 | when 'hcl' 63 | # I should be able to use require_resource above but it doesn't work 64 | if input('products_used').include?("Terraform") 65 | describe terraform_syntax(hcl: section.value) do 66 | it { should be_valid } 67 | end 68 | else 69 | describe hcl_syntax(hcl: section.value) do 70 | it { should be_valid } 71 | end 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /profiles/nomad/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: nomad 3 | title: Nomad profile 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This profile runs all commands found in the nomad directory 9 | version: 2.0.0 10 | depends: 11 | - name: shared 12 | path: ../shared 13 | inputs: 14 | - name: replacements 15 | type: hash 16 | value: 17 | '': 's.KkNJYWF5g0pomcCLEmDdOVCW' 18 | - name: products_used 19 | type: array 20 | value: ['Nomad','Nomad Enterprise'] 21 | - name: files_to_check 22 | value: [] 23 | -------------------------------------------------------------------------------- /profiles/packer/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```shell 4 | ./run.sh -p packer -d ~/src/learn 5 | ``` 6 | -------------------------------------------------------------------------------- /profiles/packer/controls/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | markdown_glob: 'content/tutorials/packer/**/*.mdx' 6 | -------------------------------------------------------------------------------- /profiles/packer/controls/validate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | require 'getoptlong' 6 | require 'kramdown' 7 | require 'yaml' 8 | 9 | $config = YAML.load(File.read("#{__dir__}/config.yaml")) 10 | 11 | markdown_files = if input('files_to_check').empty? 12 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") 13 | else 14 | # This will return the intersection of the two arrays 15 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") && input('files_to_check') 16 | end 17 | 18 | raise "No markdown files found!}" if markdown_files.count.zero? 19 | 20 | include_controls "shared" 21 | 22 | # TODO: Can't use this as inspec.command doesn't work as inspec in nil 23 | # See if statement below, should track down why this doesn't work 24 | # require_resource(profile: 'terraform', 25 | # resource: 'terraform_syntax', 26 | # as: 'hcl_syntax') 27 | 28 | 29 | # Enumerate our markdown files 30 | markdown_files.each do |file| 31 | # Load the front matter (no parsing needed) 32 | front_matter = YAML.load_file(file) 33 | 34 | control file do 35 | impact 1.0 36 | title front_matter['name'] 37 | desc front_matter['description'] 38 | 39 | ref File.basename(file), 40 | url: 'https://github.com/hashicorp/learn/blob/master/#{file.split("/").drop(1).join("/")}' 41 | 42 | # Parse the markdown 43 | markdown = Kramdown::Document.new(File.read(file), input: 'GFM') 44 | 45 | markdown.root.children.each_with_index do |section,index| 46 | case section.type 47 | # Parse the codeblocks 48 | when :codeblock 49 | case section.options[:lang] 50 | when 'json' 51 | describe json_syntax(value: section.value) do 52 | it { should be_valid } 53 | end 54 | when /shell|shell-session/ 55 | describe shell_syntax(value: section.value, replacements: input("replacements") ) do 56 | it { should be_valid } 57 | end 58 | when 'yaml' 59 | describe yaml_syntax(value: section.value) do 60 | it { should be_valid } 61 | end 62 | when 'hcl' 63 | if input('products_used').include?("packer") 64 | describe packer_syntax(hcl: section.value) do 65 | it { should be_valid } 66 | end 67 | else 68 | describe hcl_syntax(hcl: section.value) do 69 | it { should be_valid } 70 | end 71 | end 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /profiles/packer/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: packer 3 | title: Packer profile 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This profile generates controls from packer codeblocks in markdown 9 | version: 2.0.0 10 | depends: 11 | - name: shared 12 | path: ../shared 13 | inputs: 14 | - name: replacements 15 | type: hash 16 | value: 17 | '': 's.KkNJYWF5g0pomcCLEmDdOVCW' 18 | - name: products_used 19 | type: array 20 | value: ['packer'] 21 | - name: files_to_check 22 | value: [] 23 | -------------------------------------------------------------------------------- /profiles/packer/libraries/packer_syntax.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | class PackerSyntax < Inspec.resource(1) 5 | require 'shellwords' 6 | 7 | name 'packer_syntax' 8 | 9 | desc 'Call the linter command for packer' 10 | 11 | example " 12 | describe packer_syntax(hcl: '...') do 13 | it { should be_valid } 14 | end 15 | " 16 | 17 | attr_reader :name, :filename 18 | 19 | 20 | def initialize(hcl:) 21 | @content = hcl 22 | end 23 | 24 | def valid? 25 | packer_command('fmt') 26 | end 27 | 28 | def method_missing(name) 29 | packer_command(name.to_s) 30 | end 31 | 32 | private 33 | 34 | def packer_command(action) 35 | result = inspec.command("echo #{Shellwords.escape(@content)} | 36 | terraform #{action} -").result 37 | exit_status = result.exit_status 38 | if exit_status.zero? 39 | exit_status.zero? 40 | else 41 | raise Inspec::Exceptions::ResourceFailed, 42 | "packer validate failed: \n #{@content} #{result.stderr} #{result.stdout}" 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /profiles/shared/.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /profiles/shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared Profile 2 | 3 | Mainly used for shared custom resources 4 | 5 | 6 | | Resource | Notes | 7 | | ------------- | -----------------------------------------------:| 8 | | hcl_syntax | Uses the `hclfmt` tool to do hcl syntax checks | 9 | | json_syntax | Uses the `ruby` tool to do json syntax checks | 10 | | shell_syntax | Uses the `bash` shell to do syntax checks | 11 | | yaml_syntax | Uses the `ruby` tool to do yaml syntax checks | 12 | 13 | # `shell_syntax` 14 | 15 | This resource extracts shell commands using common logic on the learn platform. 16 | 17 | -------------------------------------------------------------------------------- /profiles/shared/controls/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hashicorp/learn-inspec/1957d8302ab51062ad1b42493223aeed1356a20b/profiles/shared/controls/.gitkeep -------------------------------------------------------------------------------- /profiles/shared/controls/shared.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | -------------------------------------------------------------------------------- /profiles/shared/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: shared 3 | title: Resources Shared By All Controls 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This is a skeleton 9 | version: 1.0.0 10 | -------------------------------------------------------------------------------- /profiles/shared/libraries/README.md: -------------------------------------------------------------------------------- 1 | # Custom Resource 2 | 3 | this directory contains the custom json resource , which automates the file validation 4 | -------------------------------------------------------------------------------- /profiles/shared/libraries/hcl_syntax.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | 5 | class HclSyntax < Inspec.resource(1) 6 | 7 | require 'shellwords' 8 | 9 | name 'hcl_syntax' 10 | 11 | desc 'Checks HCL syntax' 12 | 13 | example " 14 | describe hcl_syntax(hcl: '...') do 15 | it { should be_valid } 16 | end 17 | " 18 | 19 | attr_reader :name, :filename 20 | 21 | 22 | def initialize(hcl:) 23 | @content = hcl 24 | end 25 | 26 | def valid? 27 | hclfmt_command() 28 | end 29 | 30 | private 31 | 32 | def hclfmt_command() 33 | result = inspec.command("echo #{Shellwords.escape(@content)} | 34 | hclfmt").result 35 | exit_status = result.exit_status 36 | if exit_status.zero? 37 | exit_status.zero? 38 | else 39 | raise Inspec::Exceptions::ResourceFailed, 40 | "hclfmt validate failed: \n #{@content} #{result.stderr} #{result.stdout}" 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /profiles/shared/libraries/json_syntax.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | class JsonSyntax < Inspec.resource(1) 5 | 6 | name 'json_syntax' 7 | 8 | desc 'Syntax checker for json' 9 | 10 | example " 11 | describe json_syntax(value: '...') do 12 | it { should be_valid } 13 | end 14 | " 15 | 16 | attr_reader :name 17 | 18 | def initialize(value:) 19 | @value = value 20 | if value.match(/\.\.\./) 21 | return skip_resource \ 22 | "Skipping test: cannot validate json with ellipsis: \n #{value}" 23 | end 24 | end 25 | 26 | def valid? 27 | validate_json(@value) 28 | end 29 | 30 | 31 | private 32 | 33 | def validate_json(value) 34 | result = JSON.parse(value) 35 | result.is_a?(Hash) || result.is_a?(Array) 36 | rescue JSON::ParserError, TypeError => error 37 | raise Inspec::Exceptions::ResourceFailed, 38 | "JSON is invalid: \n #{@value} \n #{error.message}" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /profiles/shared/libraries/shell_syntax.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | class ShellSyntax < Inspec.resource(1) 5 | 6 | require 'shellwords' 7 | 8 | name 'shell_syntax' 9 | 10 | desc 'Syntax checker for shell' 11 | 12 | example " 13 | describe shell_syntax(value: '...', replacements: '...'') do 14 | it { should be_valid } 15 | end 16 | " 17 | 18 | attr_reader :name 19 | 20 | def initialize(value: ,replacements:) 21 | @value = value 22 | 23 | # Fail early for any known skips 24 | if @value.lines[0].count("'") == 1 25 | return skip_resource \ 26 | "Skipping test: Can't parse single quoted multiline commands (firstline) \n #{value}" 27 | end 28 | 29 | # Attempt to parse the command 30 | @command = parse_command(replace_pseudo(value,replacements)) 31 | 32 | # Skip criteria 33 | if @command.nil? 34 | return skip_resource \ 35 | "Unable to parse shell command, codeblock lang mismatch? \n #{value}" 36 | elsif @command.match(/(<\w+.*\w+?>|\.\.\.)/) 37 | return skip_resource \ 38 | "Skipping test: Pseudo variable or ellipses detected \n #{value}" 39 | #elsif @command.lines[-1].count("'") == 1 40 | # return skip_resource \ 41 | # "Skipping test: Can't parse single quoted multiline commands (last parsed line)\n #{value}" 42 | end 43 | end 44 | 45 | def valid? 46 | validate_shell(@value,@command) 47 | end 48 | 49 | def replace_pseudo(value,replacements) 50 | value.gsub(/<\w+.*\w+?>/,replacements) 51 | end 52 | 53 | def parse_escaped_lines(escaped_lines,lines) 54 | 55 | last_line = lines.at(escaped_lines.count) 56 | 57 | # Break our codeblock out as a shellwords array 58 | shellwords = lines.join(' ').shellsplit 59 | 60 | # Create a similar break out for just the escaped lines 61 | escaped_shellwords = escaped_lines.join(' ').shellsplit 62 | 63 | # When the two don't match we likely have addtional parsing needed. 64 | if escaped_shellwords.last != shellwords.at(escaped_shellwords.count - 1) 65 | index = escaped_shellwords.count - 1 66 | 67 | parsed_command = escaped_lines.append( 68 | shellwords.at( 69 | index 70 | ).shellescape 71 | ) 72 | else 73 | parsed_command = escaped_lines.append(last_line) 74 | end 75 | 76 | parsed_command.join 77 | 78 | rescue ArgumentError => e 79 | if lines.join.scan(/^.*\$/).count > 1 80 | raise Inspec::Exceptions::ResourceFailed, 81 | "Multiple commands in a single block detected: \n #{lines.join}" 82 | else 83 | # If we can't parse the entry , assume the whole block is the command 84 | return lines.join \ 85 | if e.message.match(/Unmatched double quote/) 86 | raise Inspec::Exceptions::ResourceSkipped, 87 | "Unable to recontruct the full command: \ 88 | #{escaped_lines.join} from \n #{lines.join} \ 89 | The Error was #{e.message} \n #{e.backtrace}" 90 | end 91 | end 92 | 93 | def parse_command(value) 94 | lines = value.lines.to_a 95 | 96 | # Gather lines with escape chars at the end 97 | escaped_lines = lines.select{|line| line.chomp[-1] == '\\'} 98 | 99 | if !escaped_lines.empty? 100 | # Handle multiline commands 101 | parsed_command = parse_escaped_lines(escaped_lines, lines) 102 | elsif lines[0].lstrip[0] == '$' 103 | # Handle singleline commands 104 | parsed_command = lines[0].lstrip 105 | elsif lines[0].lstrip[0] == '#' 106 | # Handle comment at line 0 of the code block 107 | parsed_command = lines[1].lstrip if lines.count > 1 108 | 109 | # Assume anything thats a single line is a root prompt 110 | parsed_command = lines[0].sub(/^#/,'').lstrip if lines.count == 1 111 | elsif lines[0].lstrip[0] == '/' 112 | # Handle docker exec style prompts 113 | parsed_command = lines[0].lstrip.sub(%r{^/}, '').lstrip 114 | end 115 | 116 | parsed_command 117 | rescue 118 | raise "Unable to parse command \n : #{value}" 119 | end 120 | 121 | def validate_shell(value,command) 122 | 123 | # Skip if we can't parse the shell 124 | raise Inspec::Exceptions::ResourceSkipped, 125 | "Unable to parse shell: \n #{value}" if command.nil? 126 | 127 | result = inspec.command( 128 | "echo #{Shellwords.escape(command.sub!(/^\w*\$/,''))} | bash -n").result 129 | exit_status = result.exit_status 130 | 131 | if exit_status.zero? 132 | exit_status.zero? 133 | else 134 | raise Inspec::Exceptions::ResourceFailed, 135 | "Invalid Shell Syntax: \n #{value} Parsed as \n#{command} #{result.stderr} #{result.stdout}" 136 | end 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /profiles/shared/libraries/yaml_syntax.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | class YamlSyntax < Inspec.resource(1) 5 | 6 | name 'yaml_syntax' 7 | 8 | desc 'Syntax checker for yaml' 9 | 10 | example " 11 | describe yaml_syntax(value: '...') do 12 | it { should be_valid } 13 | end 14 | " 15 | 16 | attr_reader :name 17 | 18 | def initialize(value:) 19 | @value = value 20 | end 21 | 22 | def valid? 23 | validate_yaml(@value) 24 | end 25 | 26 | 27 | private 28 | 29 | def validate_yaml(value) 30 | YAML.load(value) 31 | rescue => error 32 | raise Inspec::Exceptions::ResourceFailed, 33 | "yaml is invalid: \n #{@value} \n #{error.message}" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /profiles/terraform/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```shell 4 | ./run.sh -p terraform -d ~/src/learn 5 | ``` 6 | -------------------------------------------------------------------------------- /profiles/terraform/controls/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | markdown_glob: 'content/tutorials/terraform/**/*.mdx' 6 | -------------------------------------------------------------------------------- /profiles/terraform/controls/validate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | require 'getoptlong' 6 | require 'kramdown' 7 | require 'yaml' 8 | 9 | $config = YAML.load(File.read("#{__dir__}/config.yaml")) 10 | 11 | markdown_files = if input('files_to_check').empty? 12 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") 13 | else 14 | # This will return the intersection of the two arrays 15 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") && input('files_to_check') 16 | end 17 | 18 | raise "No markdown files found!}" if markdown_files.count.zero? 19 | 20 | include_controls "shared" 21 | 22 | # TODO: Can't use this as inspec.command doesn't work as inspec in nil 23 | # See if statement below, should track down why this doesn't work 24 | # require_resource(profile: 'terraform', 25 | # resource: 'terraform_syntax', 26 | # as: 'hcl_syntax') 27 | 28 | 29 | # Enumerate our markdown files 30 | markdown_files.each do |file| 31 | # Load the front matter (no parsing needed) 32 | front_matter = YAML.load_file(file) 33 | 34 | control file do 35 | impact 1.0 36 | title front_matter['name'] 37 | desc front_matter['description'] 38 | 39 | ref File.basename(file), 40 | url: 'https://github.com/hashicorp/learn/blob/master/#{file.split("/").drop(1).join("/")}' 41 | 42 | # Parse the markdown 43 | markdown = Kramdown::Document.new(File.read(file), input: 'GFM') 44 | 45 | markdown.root.children.each_with_index do |section,index| 46 | case section.type 47 | # Parse the codeblocks 48 | when :codeblock 49 | case section.options[:lang] 50 | when 'json' 51 | describe json_syntax(value: section.value) do 52 | it { should be_valid } 53 | end 54 | when /shell|shell-session/ 55 | describe shell_syntax(value: section.value, replacements: input("replacements") ) do 56 | it { should be_valid } 57 | end 58 | when 'yaml' 59 | describe yaml_syntax(value: section.value) do 60 | it { should be_valid } 61 | end 62 | when 'hcl' 63 | # I should be able to use require_resource above but it doesn't work 64 | if input('products_used').include?("terraform") 65 | describe terraform_syntax(hcl: section.value) do 66 | it { should be_valid } 67 | end 68 | else 69 | describe hcl_syntax(hcl: section.value) do 70 | it { should be_valid } 71 | end 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /profiles/terraform/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: terraform 3 | title: Terraform profile 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This profile generates controls from terraform codeblocks in markdown 9 | version: 2.0.0 10 | depends: 11 | - name: shared 12 | path: ../shared 13 | inputs: 14 | - name: replacements 15 | type: hash 16 | value: 17 | '': 's.KkNJYWF5g0pomcCLEmDdOVCW' 18 | - name: products_used 19 | type: array 20 | value: ['terraform','Terraform Enterprise'] 21 | - name: files_to_check 22 | value: [] 23 | -------------------------------------------------------------------------------- /profiles/terraform/libraries/terraform_syntax.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | class TerraformSyntax < Inspec.resource(1) 5 | require 'shellwords' 6 | 7 | name 'terraform_syntax' 8 | 9 | desc 'A wrapper to create & destroy a tf file' 10 | 11 | example " 12 | describe terraform_syntax(hcl: '...') do 13 | it { should be_valid } 14 | end 15 | " 16 | 17 | attr_reader :name, :filename 18 | 19 | 20 | def initialize(hcl:) 21 | @content = hcl 22 | end 23 | 24 | def initialized? 25 | terraform_command('init -backend=false -input=false') 26 | end 27 | 28 | def valid? 29 | terraform_command('fmt') 30 | end 31 | 32 | def method_missing(name) 33 | terraform_command(name.to_s) 34 | end 35 | 36 | private 37 | 38 | def terraform_command(action) 39 | result = inspec.command("echo #{Shellwords.escape(@content)} | 40 | TF_IN_AUTOMATION=yes terraform #{action} -").result 41 | exit_status = result.exit_status 42 | if exit_status.zero? 43 | exit_status.zero? 44 | else 45 | raise Inspec::Exceptions::ResourceFailed, 46 | "Terraform validate failed: \n #{@content} #{result.stderr} #{result.stdout}" 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /profiles/vault.io/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```shell 4 | mkdir -p ~/src 5 | git clone git@github.com:hashicorp/vault.git ~/src/vault 6 | ``` 7 | 8 | ```shell 9 | ./run.sh -p vault.io -d ~/src/vault 10 | ``` 11 | -------------------------------------------------------------------------------- /profiles/vault.io/controls/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | markdown_glob: 'website/pages/docs/**/*.mdx' 6 | -------------------------------------------------------------------------------- /profiles/vault.io/controls/validate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | require 'getoptlong' 6 | require 'kramdown' 7 | require 'yaml' 8 | 9 | $config = YAML.load(File.read("#{__dir__}/config.yaml")) 10 | 11 | markdown_files = Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") 12 | 13 | raise "No markdown files found!}" if markdown_files.count.zero? 14 | 15 | include_controls "shared" 16 | 17 | 18 | # Enumerate our markdown files 19 | markdown_files.each do |file| 20 | # Load the front matter (no parsing needed) 21 | front_matter = YAML.load_file(file) 22 | 23 | control file do 24 | impact 1.0 25 | title front_matter['page_title'] 26 | desc front_matter['description'] 27 | 28 | ref File.basename(file), 29 | url: 'https://github.com/hashicorp/learn/blob/master/#{file.split("/").drop(1).join("/")}' 30 | 31 | 32 | # Parse the markdown 33 | markdown = Kramdown::Document.new(File.read(file), input: 'GFM') 34 | 35 | markdown.root.children.each_with_index do |section,index| 36 | case section.type 37 | # Parse the codeblocks 38 | when :codeblock 39 | case section.options[:lang] 40 | when 'json' 41 | describe json_syntax(value: section.value) do 42 | it { should be_valid } 43 | end 44 | when 'shell' 45 | describe shell_syntax(value: section.value, replacements: input("replacements") ) do 46 | it { should be_valid } 47 | end 48 | when 'yaml' 49 | describe yaml_syntax(value: section.value) do 50 | it { should be_valid } 51 | end 52 | when 'hcl' 53 | describe hcl_syntax(hcl: section.value) do 54 | it { should be_valid } 55 | end 56 | end 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /profiles/vault.io/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: vault.io 3 | title: Vault.io profile 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This profile checks the syntax of the https://vault.io website 9 | version: 2.0.0 10 | depends: 11 | - name: shared 12 | path: ../shared 13 | inputs: 14 | - name: replacements 15 | type: hash 16 | value: 17 | '': 's.KkNJYWF5g0pomcCLEmDdOVCW' 18 | - name: products_used 19 | type: array 20 | value: ['Nomad','Nomad Enterprise'] 21 | - name: files_to_check 22 | value: [] 23 | -------------------------------------------------------------------------------- /profiles/vault/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ```shell 4 | ./run.sh -p vault -d ~/src/learn 5 | ``` 6 | -------------------------------------------------------------------------------- /profiles/vault/controls/config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | --- 5 | markdown_glob: 'content/tutorials/vault/**/*.mdx' 6 | -------------------------------------------------------------------------------- /profiles/vault/controls/validate.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | require 'getoptlong' 6 | require 'kramdown' 7 | require 'yaml' 8 | 9 | $config = YAML.load(File.read("#{__dir__}/config.yaml")) 10 | 11 | markdown_files = if input('files_to_check').empty? 12 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") 13 | else 14 | # This will return the intersection of the two arrays 15 | Dir.glob("#{ENV['MARKDOWN']}/#{$config['markdown_glob']}") && input('files_to_check') 16 | end 17 | 18 | raise "No markdown files found!}" if markdown_files.count.zero? 19 | 20 | include_controls "shared" 21 | 22 | # TODO: Can't use this as inspec.command doesn't work as inspec in nil 23 | # See if statement below, should track down why this doesn't work 24 | # require_resource(profile: 'terraform', 25 | # resource: 'terraform_syntax', 26 | # as: 'hcl_syntax') 27 | 28 | 29 | # Enumerate our markdown files 30 | markdown_files.each do |file| 31 | # Load the front matter (no parsing needed) 32 | front_matter = YAML.load_file(file) 33 | 34 | control file do 35 | impact 1.0 36 | title front_matter['name'] 37 | desc front_matter['description'] 38 | 39 | ref File.basename(file), 40 | url: 'https://github.com/hashicorp/learn/blob/master/#{file.split("/").drop(1).join("/")}' 41 | 42 | # Parse the markdown 43 | markdown = Kramdown::Document.new(File.read(file), input: 'GFM') 44 | 45 | markdown.root.children.each_with_index do |section,index| 46 | case section.type 47 | # Parse the codeblocks 48 | when :codeblock 49 | case section.options[:lang] 50 | when 'json' 51 | describe json_syntax(value: section.value) do 52 | it { should be_valid } 53 | end 54 | when /shell|shell-session/ 55 | describe shell_syntax(value: section.value, replacements: input("replacements") ) do 56 | it { should be_valid } 57 | end 58 | when 'yaml' 59 | describe yaml_syntax(value: section.value) do 60 | it { should be_valid } 61 | end 62 | when 'hcl' 63 | # I should be able to use require_resource above but it doesn't work 64 | if input('products_used').include?("Terraform") 65 | describe terraform_syntax(hcl: section.value) do 66 | it { should be_valid } 67 | end 68 | else 69 | describe hcl_syntax(hcl: section.value) do 70 | it { should be_valid } 71 | end 72 | end 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /profiles/vault/inspec.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: vault 3 | title: Vault profile 4 | maintainer: Zack Smith 5 | copyright: Hashicorp Inc. 6 | copyright_email: zsmith@hashicorp.com 7 | license: Apache-2.0 8 | summary: This profile runs all commands found in the vault directory 9 | version: 2.0.0 10 | depends: 11 | - name: shared 12 | path: ../shared 13 | inputs: 14 | - name: replacements 15 | type: hash 16 | value: 17 | '': 's.KkNJYWF5g0pomcCLEmDdOVCW' 18 | - name: products_used 19 | type: array 20 | value: ['Vault','Vault Enterprise'] 21 | - name: files_to_check 22 | value: [] 23 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | trap cleanup EXIT 7 | trap cleanup TERM 8 | trap cleanup ERR 9 | 10 | # Run our ruby environment in docker for portablity 11 | function inspec() { 12 | docker run \ 13 | -e MARKDOWN=/markdown \ 14 | -v "${LEARN_DIR:?"Pass with -d"}":/markdown \ 15 | -v /tmp:/tmp \ 16 | -v /var/run/docker.sock:/var/run/docker.sock \ 17 | inspec \ 18 | "$@" 19 | } 20 | 21 | # Parse our options 22 | function help() { 23 | echo < Building Docker image..." 18 | @docker build \ 19 | --rm \ 20 | -t \ 21 | $(IMAGE_NAME):$(DOCKER_TAG) \ 22 | . 23 | push: build 24 | # open onepassword://search/ej6uodvxxvcetihbgwdy2wnoh4 25 | # export DOCKER_API_TOKEN= 26 | @echo $(DOCKER_API_TOKEN) | \ 27 | docker login --username $(DOCKER_USERNAME) --password-stdin 28 | @docker tag $(call GetImageID,$(IMAGE_NAME):$(DOCKER_TAG)) \ 29 | $(DOCKER_USERNAME)/$(IMAGE_NAME):$(DOCKER_TAG) 30 | @docker push \ 31 | $(DOCKER_USERNAME)/$(IMAGE_NAME):$(DOCKER_TAG) 32 | 33 | .DEFAULT_GOAL := build 34 | 35 | .PHONY: build push 36 | -------------------------------------------------------------------------------- /target/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This directory contains a docker image that is used as a target for the inspec tests. 4 | 5 | 6 | ## Usage 7 | 8 | This container is automatically provisioned when run via the `run.sh` script using the `make` command. This container can also be manually spun up to allow for debugging failing tests. 9 | 10 | ## Launching the inspec-target container interactively 11 | 12 | If you wish to debug failures of the inspec tests you may wish to manually run the syntax commands on the target container. 13 | First run the `interactive.sh` script shown below. This script will build an `inspec-target` container and run an interactive shell in that container. This interactive shell must be running for inspec to connect to the container. This shell can also be used to debug failed tests conditions. 14 | 15 | ```shell 16 | ./target/interactive.sh 17 | ``` 18 | 19 | > :exclamation: Do not close this window and proceed to the next step. 20 | 21 | You can modify the `interactive.sh` to pass environmental variables that are required for your test examples. 22 | -------------------------------------------------------------------------------- /target/background.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | # Allow us to call script from any pwd 7 | SCRIPT_DIR="${0%/*}" 8 | 9 | cd "$SCRIPT_DIR" 10 | 11 | # Build inspec-target container 12 | make 13 | 14 | # Refresh namespace 15 | docker rm -f inspec-target &>/dev/null 16 | 17 | # Run container with stub environmental vars 18 | # Override the infinate entry point so users 19 | # can do an interactive debug session with sh 20 | docker run \ 21 | -d \ 22 | -e VAULT_ADDR='http://127.0.0.1:8200' \ 23 | --name inspec-target \ 24 | inspec-target 25 | -------------------------------------------------------------------------------- /target/future/Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_NAME="inspec-target" 2 | 3 | define GetImageID 4 | `docker images --filter=reference=$(1) --format "{{.ID}}"` 5 | endef 6 | 7 | build: 8 | @echo "==> Building Docker image..." 9 | @echo $(GITHUB_TOKEN) | \ 10 | docker login docker.pkg.github.com -u acidprime --password-stdin 11 | @docker build \ 12 | --rm \ 13 | -t \ 14 | $(IMAGE_NAME) \ 15 | . 16 | @docker tag $(call GetImageID,$(IMAGE_NAME)) \ 17 | docker.pkg.github.com/hashicorp/learn-inspec/$(IMAGE_NAME):latest 18 | @docker push \ 19 | docker.pkg.github.com/hashicorp/learn-inspec/$(IMAGE_NAME):latest 20 | 21 | .DEFAULT_GOAL := build 22 | 23 | .PHONY: build 24 | -------------------------------------------------------------------------------- /target/interactive.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | # Copyright (c) HashiCorp, Inc. 3 | # SPDX-License-Identifier: MPL-2.0 4 | 5 | 6 | # Allow us to call script from any pwd 7 | SCRIPT_DIR="${0%/*}" 8 | 9 | cd "$SCRIPT_DIR" 10 | 11 | # Build inspec-target container 12 | make 13 | 14 | # Refresh namespace 15 | docker rm -f inspec-target &>/dev/null 16 | 17 | # Run container with stub environmental vars 18 | # Override the infinate entry point so users 19 | # can do an interactive debug session with sh 20 | docker run \ 21 | --entrypoint '/bin/sh' \ 22 | -e VAULT_ADDR='http://127.0.0.1:8200' \ 23 | --name inspec-target \ 24 | -ti inspec-target 25 | --------------------------------------------------------------------------------