diff --git a/docs/NPM-PACKAGE-AGE-POLICY.md b/docs/NPM-PACKAGE-AGE-POLICY.md new file mode 100644 index 0000000..429c712 --- /dev/null +++ b/docs/NPM-PACKAGE-AGE-POLICY.md @@ -0,0 +1,220 @@ +# NPM Package Age Policy + +## Requirement + +All npm packages must be **at least 2 weeks old** before they can be used in this project. This ensures: +- Package stability +- Security vulnerability disclosure time +- Compliance with organizational security policies + +## How It Works + +### 1. Package Version Pinning + +The project uses exact version pinning in `package.json` to prevent automatic updates: + +```json +{ + "dependencies": { + "@angular/core": "19.2.7", // Exact version, not "^19.2.7" or "~19.2.7" + // ... + } +} +``` + +The `frontend/.npmrc` file enforces this: +``` +save-exact=true +package-lock=true +``` + +### 2. Automated Age Checking + +Use the provided script to verify all packages meet the 2-week requirement: + +```bash +# Check if all packages are at least 2 weeks old +node scripts/check-package-age.js +``` + +This script: +- Queries npm registry for publish dates +- Calculates age of each package +- Fails if any package is newer than 14 days +- Shows detailed age information for all packages + +### 3. Installation Process + +**Always use `npm ci` instead of `npm install`:** + +```bash +cd frontend +npm ci # Installs exact versions from package-lock.json +``` + +**Why `npm ci`?** +- Uses exact versions from `package-lock.json` +- Doesn't update `package-lock.json` +- Ensures reproducible builds +- Faster than `npm install` + +## Updating Packages + +When you need to add or update packages: + +### Step 1: Add Package to package.json + +```bash +# Find a version that's at least 2 weeks old +npm view time + +# Add exact version to package.json +"dependencies": { + "new-package": "1.2.3" +} +``` + +### Step 2: Verify Age + +```bash +node scripts/check-package-age.js +``` + +### Step 3: Update Lock File + +```bash +cd frontend +npm install --package-lock-only +``` + +### Step 4: Install and Test + +```bash +npm ci +npm run build:prod +``` + +## CI/CD Integration + +Add the age check to your CI/CD pipeline: + +### GitLab CI Example + +```yaml +verify_package_age: + stage: validate + image: node:18-alpine + script: + - node scripts/check-package-age.js + only: + - merge_requests + - main +``` + +### GitHub Actions Example + +```yaml +- name: Check Package Age + run: node scripts/check-package-age.js +``` + +## Troubleshooting + +### "Package is too new" Error + +If a package fails the age check: + +1. **Find an older version:** + ```bash + npm view versions --json + npm view @ time + ``` + +2. **Update package.json with older version** + +3. **Re-run age check:** + ```bash + node scripts/check-package-age.js + ``` + +### Can't Find Old Enough Version + +If no version meets the 2-week requirement: +- Wait until the package is at least 2 weeks old +- Look for alternative packages +- Request an exception through your security team + +## Example Workflow + +```bash +# 1. Check current package ages +node scripts/check-package-age.js + +# 2. If all pass, install dependencies +cd frontend +npm ci + +# 3. Build application +npm run build:prod + +# 4. For air-gapped deployment +../scripts/build-for-airgap.sh +``` + +## Scripts Reference + +| Script | Purpose | +|--------|---------| +| `scripts/check-package-age.js` | Verify all packages are ≥ 2 weeks old | +| `scripts/pin-old-versions.sh` | Helper script to validate and pin versions | +| `scripts/build-for-airgap.sh` | Build frontend for air-gapped deployment | + +## Best Practices + +1. **Always commit `package-lock.json`** + - Ensures everyone uses the same versions + - Required for reproducible builds + +2. **Use `npm ci` in CI/CD** + - Faster than `npm install` + - Enforces lock file versions + - Prevents surprises + +3. **Regular audits** + ```bash + # Check for security vulnerabilities + npm audit + + # Check package ages + node scripts/check-package-age.js + ``` + +4. **Version ranges to avoid** + - ❌ `^1.2.3` (allows minor/patch updates) + - ❌ `~1.2.3` (allows patch updates) + - ❌ `*` or `latest` (allows any version) + - ✅ `1.2.3` (exact version only) + +## Package Age Check Output + +``` +Checking package ages (must be at least 2 weeks old)... + +✓ @angular/common@19.2.7 - 45 days old +✓ @angular/compiler@19.2.7 - 45 days old +✓ rxjs@7.8.0 - 180 days old +❌ new-package@1.0.0 - 5 days old (published 2025-01-12) + +================================================================================ + +❌ FAILED: 1 package(s) are newer than 2 weeks: + + - new-package@1.0.0 (5 days old, published 2025-01-12) +``` + +## Support + +For questions or exceptions: +- Review with security team +- Document in project README +- Update this policy as needed diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..a41f07f --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,11 @@ +# Force exact version installation (no version range resolution) +save-exact=true + +# Always use package-lock.json +package-lock=true + +# Don't automatically update package-lock.json +package-lock-only=false + +# Prevent automatic updates +save-prefix='' diff --git a/scripts/check-package-age.js b/scripts/check-package-age.js new file mode 100755 index 0000000..ef1b9c1 --- /dev/null +++ b/scripts/check-package-age.js @@ -0,0 +1,102 @@ +#!/usr/bin/env node + +/** + * Check if npm packages are at least 2 weeks old before installation + * Usage: node scripts/check-package-age.js + */ + +const https = require('https'); +const fs = require('fs'); +const path = require('path'); + +const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000; + +function getPackageInfo(packageName, version) { + return new Promise((resolve, reject) => { + // Remove version prefixes like ^, ~, >= + const cleanVersion = version.replace(/^[\^~>=]+/, ''); + + const url = `https://registry.npmjs.org/${packageName}/${cleanVersion}`; + + https.get(url, (res) => { + let data = ''; + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', () => { + try { + const info = JSON.parse(data); + resolve({ + name: packageName, + version: cleanVersion, + published: info.time || info._time, + error: null + }); + } catch (err) { + reject(new Error(`Failed to parse response for ${packageName}@${cleanVersion}`)); + } + }); + }).on('error', (err) => { + reject(err); + }); + }); +} + +async function checkPackageAge() { + const packageJsonPath = path.join(__dirname, '../frontend/package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + const allDeps = { + ...packageJson.dependencies, + ...packageJson.devDependencies + }; + + console.log('Checking package ages (must be at least 2 weeks old)...\n'); + + const tooNew = []; + const errors = []; + + for (const [name, version] of Object.entries(allDeps)) { + try { + const info = await getPackageInfo(name, version); + const publishDate = new Date(info.published); + const age = Date.now() - publishDate.getTime(); + const ageInDays = Math.floor(age / (24 * 60 * 60 * 1000)); + + if (age < TWO_WEEKS_MS) { + tooNew.push({ + name, + version: info.version, + age: ageInDays, + published: publishDate.toISOString().split('T')[0] + }); + console.log(`❌ ${name}@${info.version} - ${ageInDays} days old (published ${publishDate.toISOString().split('T')[0]})`); + } else { + console.log(`✓ ${name}@${info.version} - ${ageInDays} days old`); + } + } catch (err) { + errors.push({ name, version, error: err.message }); + console.log(`⚠️ ${name}@${version} - Could not check: ${err.message}`); + } + } + + console.log('\n' + '='.repeat(80)); + + if (tooNew.length > 0) { + console.log(`\n❌ FAILED: ${tooNew.length} package(s) are newer than 2 weeks:\n`); + tooNew.forEach(pkg => { + console.log(` - ${pkg.name}@${pkg.version} (${pkg.age} days old, published ${pkg.published})`); + }); + process.exit(1); + } else if (errors.length > 0) { + console.log(`\n⚠️ WARNING: Could not verify ${errors.length} package(s)`); + process.exit(0); + } else { + console.log('\n✓ SUCCESS: All packages are at least 2 weeks old'); + process.exit(0); + } +} + +checkPackageAge(); diff --git a/scripts/pin-old-versions.sh b/scripts/pin-old-versions.sh new file mode 100755 index 0000000..9dafbd2 --- /dev/null +++ b/scripts/pin-old-versions.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Pin npm packages to versions that are at least 2 weeks old +# This script helps ensure compliance with package age requirements + +set -e + +echo "=========================================" +echo "Pin NPM Packages to Old Versions" +echo "=========================================" +echo "" + +cd frontend + +echo "Step 1: Checking current package ages..." +node ../scripts/check-package-age.js || { + echo "" + echo "Some packages are too new. Recommendations:" + echo "1. Manually downgrade packages in package.json to older versions" + echo "2. Run: npm install --package-lock-only to update lock file" + echo "3. Re-run this script to verify" + exit 1 +} + +echo "" +echo "Step 2: Ensuring package-lock.json uses exact versions..." +if [ -f "package-lock.json" ]; then + echo "✓ package-lock.json exists" +else + echo "⚠ package-lock.json does not exist. Creating it..." + npm install --package-lock-only +fi + +echo "" +echo "=========================================" +echo "✓ All packages meet the 2-week age requirement" +echo "=========================================" +echo "" +echo "To install these packages:" +echo " npm ci # Uses exact versions from package-lock.json" +echo ""