Skip to content
Open
8 changes: 4 additions & 4 deletions .devcontainer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,12 @@ A pre-configured development environment that includes all tools, extensions, an
### Languages & Runtimes

- Node.js (LTS)
- Python 3.11
- PowerShell 7.x

### CLI Tools

- Git
- GitHub CLI (`gh`)
- Azure CLI (`az`)

### Code Quality

Expand Down Expand Up @@ -93,9 +91,11 @@ gitleaks detect --source . --verbose

## Troubleshooting

**Container won't build**: Ensure Docker Desktop is running and you have sufficient disk space (5GB+).
1. **Container won't build**: Ensure Docker Desktop is running and you have sufficient disk space (5GB+).

**Extensions not loading**: Reload the window (`F1` → **Developer: Reload Window**).
2. **Extensions not loading**: Reload the window (`F1` → **Developer: Reload Window**).

3. **HTTP/TLS errors during build**: Machines with corporate firewalls performing TLS inspection will need to drop their corporate root trust certificate (PEM-formatted with `.crt` extension) file into the `.devcontainer` and rebuild the devcontainer. Also run `docker buildx use desktop-linux` to ensure you are using the default builder, which honors OS root certificate trust stores.

For more help, see [SUPPORT.md](../SUPPORT.md).

Expand Down
41 changes: 36 additions & 5 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
{
"name": "HVE Core - Markdown Editing",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
// Rename the mount to /workspace for a predictable workspace paths in our scripts.
// The source path might also contain special characters, so it needs escaped double quotes.
"workspaceMount": "\"source=${localWorkspaceFolder}\",target=/workspace,type=bind",
"workspaceFolder": "/workspace",
"mounts": [
// Put GitHub local user data in a volume
{
"type": "volume",
"source": "${devcontainerId}-userconfig",
"target": "/home/vscode/.config"
},
// Put node modules into volume for better performance
{
"type": "volume",
"source": "${devcontainerId}-nodemodules",
"target": "/workspace/node_modules"
}
],
"containerEnv": {
"REQUESTS_CA_BUNDLE": "/etc/ssl/certs/ca-certificates.crt", // for pip
"NODE_EXTRA_CA_CERTS": "/etc/ssl/certs/ca-certificates.crt", // for nodejs
"SSL_CERT_FILE": "/etc/ssl/certs/ca-certificates.crt" // for uv (else use --native-tls flag)
},
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "lts"
},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.11"
},
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/azure-cli:1": {},
"ghcr.io/devcontainers/features/powershell:1": {}
},
"customizations": {
Expand All @@ -23,9 +42,21 @@
"bierner.markdown-mermaid",
"bpruitt-goddard.mermaid-markdown-syntax-highlighting",
"github.vscode-pull-request-github"
]
],
"settings": {
// Prevent extensions from stealing focus, see microsoft/vscode#205225
"workbench.view.showQuietly": {
"workbench.panel.output": true
}
}
}
},
// This is to ensure support for config includes is properly handled, see microsoft/vscode-remote-release#2084
"initializeCommand": {
"extractGitGlobals": "(git config -l --global --include || true) > .gitconfig.global",
"extractGitLocals": "(git config -l --local --include || true) > .gitconfig.local"
},
"postAttachCommand": "/bin/bash .devcontainer/scripts/post-attach.sh",
"onCreateCommand": "bash .devcontainer/scripts/on-create.sh",
"postCreateCommand": "bash .devcontainer/scripts/post-create.sh",
"remoteUser": "vscode"
Expand Down
28 changes: 28 additions & 0 deletions .devcontainer/scripts/post-attach.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail

# devcontainers copy your local gitconfig but do not parse conditional includes.
# This re-configures the devcontainer git identities based on the prior exported
# global and local git configurations *after* parsing host includes. See also:
# https://github.com/microsoft/vscode-remote-release/issues/2084#issuecomment-2289987894
copy_user_gitconfig() {
for conf in .gitconfig.global .gitconfig.local; do
if [[ -f "$conf" ]]; then
echo "*** Parsing ${conf##.gitconfig.} Git configuration export"
while IFS='=' read -r key value; do
local key value
Comment on lines +9 to +13
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local keyword should be declared before the variable assignment in the while read loop. Move the local key value declaration outside and before the loop to avoid redeclaring on each iteration.

Suggested fix:

copy_user_gitconfig() {
  local key value
  for conf in .gitconfig.global .gitconfig.local; do
    if [[ -f "$conf" ]]; then
      echo "*** Parsing ${conf##.gitconfig.} Git configuration export"
      while IFS='=' read -r key value; do
        case "$key" in
        user.name | user.email | user.signingkey | commit.gpgsign)
          echo "Set Git config ${key}=${value}"
          git config --global "$key" "$value"
          ;;
        esac
      done < "$conf"
      rm -f "${conf}"
    fi
  done
}
Suggested change
for conf in .gitconfig.global .gitconfig.local; do
if [[ -f "$conf" ]]; then
echo "*** Parsing ${conf##.gitconfig.} Git configuration export"
while IFS='=' read -r key value; do
local key value
local key value
for conf in .gitconfig.global .gitconfig.local; do
if [[ -f "$conf" ]]; then
echo "*** Parsing ${conf##.gitconfig.} Git configuration export"
while IFS='=' read -r key value; do

Copilot uses AI. Check for mistakes.
case "$key" in
user.name | user.email | user.signingkey | commit.gpgsign)
echo "Set Git config ${key}=${value}"
git config --global "$key" "$value"
;;
esac
done < "$conf"
rm -f "${conf}"
fi
done
}

# Main execution path

copy_user_gitconfig
41 changes: 40 additions & 1 deletion .devcontainer/scripts/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,49 @@

set -euo pipefail

main() {
# Volume ownership is not set automatically due to a bug:
# https://github.com/microsoft/vscode-remote-release/issues/9931
#
# IMPORTANT: workaround requires Docker base image to have password-less sudo.
fix_volume_ownership() {
local volume_path="$1"

if [[ ! -d "$volume_path" ]]; then
echo "ERROR: the volume path provided '$volume_path' does not exist."
exit 1
fi

echo "Setting volume ownership for $volume_path"
sudo chown "$USER:$USER" "$volume_path"
}

fix_volume_ownerships() {
echo "Applying volume ownership workaround (see microsoft/vscode-remote-release#9931)..."
fix_volume_ownership "/home/${USER}/.config"
fix_volume_ownership "/workspace/node_modules"
}

npm_install() {
echo "Installing NPM dependencies..."
npm install
echo "NPM dependencies installed successfully"
}

update_ca_certs() {
# Adds a root CA to the system certificate store. Useful if developer machines
# have MITM TLS inspection happening, e.g. with ZScaler.
echo "Updating container system CA certificates..."
if compgen -G ".devcontainer/*.crt" > /dev/null; then
sudo cp .devcontainer/*.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
fi
echo "Container's system CA certificates updated successfully"
Comment on lines +37 to +44
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The update_ca_certs function trusts any .crt file present in .devcontainer/ by copying it into the system CA store. This enables an attacker to commit a malicious root CA to the repo, causing the container to trust attacker-controlled TLS endpoints (e.g., for npm, pip, git), enabling MITM and credential/token theft. Restrict trusted CAs to a pre-approved, immutable source (e.g., mount a secure host-managed CA volume or require explicit opt-in via a verified checksum), and validate certificates before installation (e.g., whitelist filenames and verify SHA256).

Suggested change
# Adds a root CA to the system certificate store. Useful if developer machines
# have MITM TLS inspection happening, e.g. with ZScaler.
echo "Updating container system CA certificates..."
if compgen -G ".devcontainer/*.crt" > /dev/null; then
sudo cp .devcontainer/*.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
fi
echo "Container's system CA certificates updated successfully"
# Adds a root CA to the system certificate store, but only if it matches a pre-approved whitelist.
# This prevents malicious CAs from being trusted.
echo "Updating container system CA certificates..."
# Define whitelist: filename:sha256sum (add entries as needed)
declare -A CA_CERT_WHITELIST=(
# Example entry (replace with your actual cert and hash):
# ["my-org-root.crt"]="0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
)
local found_valid=0
for certfile in .devcontainer/*.crt; do
if [[ ! -f "${certfile}" ]]; then
continue
fi
certname="$(basename "${certfile}")"
expected_hash="${CA_CERT_WHITELIST[${certname}]:-}"
if [[ -z "${expected_hash}" ]]; then
echo "WARNING: Skipping unapproved CA certificate: ${certname}"
continue
fi
actual_hash="$(sha256sum "${certfile}" | awk '{print $1}')"
if [[ "${actual_hash}" != "${expected_hash}" ]]; then
echo "WARNING: Hash mismatch for ${certname}, skipping."
continue
fi
echo "Installing approved CA certificate: ${certname}"
sudo cp "${certfile}" /usr/local/share/ca-certificates/
found_valid=1
done
if (( found_valid )); then
sudo update-ca-certificates
echo "Container's system CA certificates updated successfully"
else
echo "No valid CA certificates installed."
fi

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WilliamBerryiii thoughts? I'm inclined to consider this a non-risk, if an attacker has access to the repo they can do far worse damage by updating any of the initialization scripts for the dev container.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love perspective from @agreaves-ms on this ... I'm on the fence here (mainly because I'm on windows and mucking with the cert store can be a PITA if things go sideways).

}

main() {
fix_volume_ownerships
npm_install
update_ca_certs
}

main "$@"
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**/.git/
**/node_modules/
10 changes: 9 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# Set the default behavior, in case core.autocrlf has not been set.
* text=auto

# Declare files that will always have LF line endings on checkout.
# Declare files that must have specific line endings on checkout.
## Windows scripts - must be CRLF
*.ps1 text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf

## Linux scripts - must be LF
*.sh text eol=lf
*.Dockerfile text eol=lf
Dockerfile* text eol=lf

# Denote all files that are truly binary and should not be modified.
*.docx binary
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,8 @@ pr-reference.xml
.mcp/*-local.json
.mcp/*.local.json
.mcp/.env

# devcontainer
/.gitconfig.global
/.gitconfig.local
/.devcontainer/*.crt
Loading