Coverage for fastblocks/mcp/env_manager.py: 0%

257 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-09 00:47 -0700

1"""Environment variable management system for FastBlocks configuration.""" 

2 

3import os 

4import re 

5from dataclasses import dataclass, field 

6from pathlib import Path 

7from typing import Any 

8 

9from .configuration import ConfigurationSchema, EnvironmentVariable 

10 

11 

12@dataclass 

13class EnvironmentValidationResult: 

14 """Result of environment variable validation.""" 

15 

16 valid: bool = True 

17 missing_required: list[str] = field(default_factory=list) 

18 invalid_format: list[str] = field(default_factory=list) 

19 security_warnings: list[str] = field(default_factory=list) 

20 recommendations: list[str] = field(default_factory=list) 

21 

22 

23@dataclass 

24class EnvironmentTemplate: 

25 """Template for environment variable generation.""" 

26 

27 name: str 

28 description: str 

29 variables: list[EnvironmentVariable] = field(default_factory=list) 

30 example_values: dict[str, str] = field(default_factory=dict) 

31 

32 

33class EnvironmentManager: 

34 """Comprehensive environment variable management for FastBlocks.""" 

35 

36 def __init__(self, base_path: Path | None = None): 

37 """Initialize environment manager.""" 

38 self.base_path = base_path or Path.cwd() 

39 self.env_dir = self.base_path / ".fastblocks" / "env" 

40 self.env_dir.mkdir(parents=True, exist_ok=True) 

41 

42 # Common validation patterns 

43 self.validation_patterns = { 

44 "url": re.compile( 

45 r"^https?://[^\s/$.?#].[^\s]*$" 

46 ), # REGEX OK: URL validation 

47 "email": re.compile( 

48 r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" 

49 ), # REGEX OK: email validation 

50 "port": re.compile( # REGEX OK: port number validation 

51 r"^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$" 

52 ), 

53 "uuid": re.compile( # REGEX OK: UUID format validation 

54 r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" 

55 ), 

56 "secret_key": re.compile( 

57 r"^[A-Za-z0-9+/]{32,}$" 

58 ), # REGEX OK: secret key format validation 

59 "path": re.compile(r"^[/~].*"), # REGEX OK: file path validation 

60 "boolean": re.compile( 

61 r"^(true|false|1|0|yes|no|on|off)$", re.IGNORECASE 

62 ), # REGEX OK: boolean value validation 

63 "log_level": re.compile( # REGEX OK: log level validation 

64 r"^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$", re.IGNORECASE 

65 ), 

66 } 

67 

68 # Security-sensitive variable patterns 

69 self.secret_patterns = [ 

70 re.compile( 

71 r".*secret.*", re.IGNORECASE 

72 ), # REGEX OK: detect secret env vars 

73 re.compile( 

74 r".*password.*", re.IGNORECASE 

75 ), # REGEX OK: detect password env vars 

76 re.compile(r".*key$", re.IGNORECASE), # REGEX OK: detect key env vars 

77 re.compile(r".*token.*", re.IGNORECASE), # REGEX OK: detect token env vars 

78 re.compile( 

79 r".*api_key.*", re.IGNORECASE 

80 ), # REGEX OK: detect API key env vars 

81 re.compile( 

82 r".*private.*", re.IGNORECASE 

83 ), # REGEX OK: detect private env vars 

84 re.compile( 

85 r".*credential.*", re.IGNORECASE 

86 ), # REGEX OK: detect credential env vars 

87 ] 

88 

89 def _check_required_variable( 

90 self, 

91 var: EnvironmentVariable, 

92 current_env: dict[str, str], 

93 result: EnvironmentValidationResult, 

94 ) -> None: 

95 """Check if required variable is present.""" 

96 if ( 

97 var.required 

98 and var.name not in current_env 

99 and not var.value 

100 and not var.default 

101 ): 

102 result.missing_required.append(var.name) 

103 result.valid = False 

104 

105 def _validate_variable_pattern( 

106 self, 

107 var: EnvironmentVariable, 

108 actual_value: str, 

109 result: EnvironmentValidationResult, 

110 ) -> None: 

111 """Validate variable against its pattern.""" 

112 if var.validator_pattern: 

113 if not re.match( 

114 var.validator_pattern, actual_value 

115 ): # REGEX OK: custom validator pattern from config 

116 result.invalid_format.append( 

117 f"{var.name}: does not match pattern {var.validator_pattern}" 

118 ) 

119 result.valid = False 

120 

121 def validate_environment_variables( 

122 self, 

123 variables: list[EnvironmentVariable], 

124 current_env: dict[str, str] | None = None, 

125 ) -> EnvironmentValidationResult: 

126 """Validate environment variables comprehensively.""" 

127 result = EnvironmentValidationResult() 

128 current_env = current_env or os.environ.copy() 

129 

130 for var in variables: 

131 # Check if required variables are present 

132 self._check_required_variable(var, current_env, result) 

133 

134 # Get actual value for validation 

135 actual_value = current_env.get(var.name) or var.value or var.default 

136 

137 if actual_value: 

138 # Validate format if pattern is specified 

139 self._validate_variable_pattern(var, actual_value, result) 

140 

141 # Check against common patterns 

142 self._validate_common_patterns(var.name, actual_value, result) 

143 

144 # Security checks 

145 self._perform_security_checks( 

146 var.name, actual_value, var.secret, result 

147 ) 

148 

149 # Additional recommendations 

150 self._generate_recommendations(variables, result) 

151 

152 return result 

153 

154 def _validate_common_patterns( 

155 self, name: str, value: str, result: EnvironmentValidationResult 

156 ) -> None: 

157 """Validate against common patterns.""" 

158 name_lower = name.lower() 

159 

160 self._validate_format_patterns(name, name_lower, value, result) 

161 self._validate_uuid_pattern(name, name_lower, value, result) 

162 self._validate_boolean_pattern(name, name_lower, value, result) 

163 self._validate_log_level_pattern(name, name_lower, value, result) 

164 

165 def _validate_format_patterns( 

166 self, 

167 name: str, 

168 name_lower: str, 

169 value: str, 

170 result: EnvironmentValidationResult, 

171 ) -> None: 

172 """Validate URL, email, and port formats.""" 

173 if "url" in name_lower and not self.validation_patterns["url"].match(value): 

174 result.invalid_format.append(f"{name}: invalid URL format") 

175 

176 if "email" in name_lower and not self.validation_patterns["email"].match(value): 

177 result.invalid_format.append(f"{name}: invalid email format") 

178 

179 if "port" in name_lower and not self.validation_patterns["port"].match(value): 

180 result.invalid_format.append(f"{name}: invalid port number") 

181 

182 def _validate_uuid_pattern( 

183 self, 

184 name: str, 

185 name_lower: str, 

186 value: str, 

187 result: EnvironmentValidationResult, 

188 ) -> None: 

189 """Validate UUID format.""" 

190 if "uuid" in name_lower or "id" in name_lower: 

191 if not self.validation_patterns["uuid"].match(value): 

192 result.recommendations.append(f"{name}: consider using UUID format") 

193 

194 def _validate_boolean_pattern( 

195 self, 

196 name: str, 

197 name_lower: str, 

198 value: str, 

199 result: EnvironmentValidationResult, 

200 ) -> None: 

201 """Validate boolean values.""" 

202 if ( 

203 "debug" in name_lower 

204 or "enable" in name_lower 

205 or name_lower.endswith("_flag") 

206 ): 

207 if not self.validation_patterns["boolean"].match(value): 

208 result.invalid_format.append( 

209 f"{name}: should be boolean (true/false, 1/0, yes/no)" 

210 ) 

211 

212 def _validate_log_level_pattern( 

213 self, 

214 name: str, 

215 name_lower: str, 

216 value: str, 

217 result: EnvironmentValidationResult, 

218 ) -> None: 

219 """Validate log level values.""" 

220 if "log" in name_lower and "level" in name_lower: 

221 if not self.validation_patterns["log_level"].match(value): 

222 result.invalid_format.append(f"{name}: invalid log level") 

223 

224 def _check_secret_strength( 

225 self, 

226 name: str, 

227 value: str, 

228 result: EnvironmentValidationResult, 

229 ) -> None: 

230 """Check strength of secret values.""" 

231 if len(value) < 16: 

232 result.security_warnings.append( 

233 f"{name}: secret appears too short (minimum 16 characters recommended)" 

234 ) 

235 

236 if value.lower() in ( 

237 "password", 

238 "secret", 

239 "key", 

240 "admin", 

241 "test", 

242 "development", 

243 ): 

244 result.security_warnings.append(f"{name}: using common/weak secret value") 

245 

246 if re.match( 

247 r"^(123|abc|test|dev)", value, re.IGNORECASE 

248 ): # REGEX OK: detect weak secrets 

249 result.security_warnings.append( 

250 f"{name}: secret appears to use predictable pattern" 

251 ) 

252 

253 def _perform_security_checks( 

254 self, 

255 name: str, 

256 value: str, 

257 is_marked_secret: bool, 

258 result: EnvironmentValidationResult, 

259 ) -> None: 

260 """Perform security checks on environment variables.""" 

261 # Check if variable should be marked as secret 

262 is_potentially_secret = any( 

263 pattern.match(name) for pattern in self.secret_patterns 

264 ) 

265 

266 if is_potentially_secret and not is_marked_secret: 

267 result.security_warnings.append( 

268 f"{name}: appears to contain sensitive data but not marked as secret" 

269 ) 

270 

271 # Check for weak secrets 

272 if is_marked_secret or is_potentially_secret: 

273 self._check_secret_strength(name, value, result) 

274 

275 # Check for exposed secrets in non-secret variables 

276 if not (is_marked_secret or is_potentially_secret): 

277 if len(value) > 20 and re.match( 

278 r"^[A-Za-z0-9+/=]+$", value 

279 ): # REGEX OK: detect potential base64-encoded secrets 

280 result.security_warnings.append( 

281 f"{name}: value looks like encoded data but not marked as secret" 

282 ) 

283 

284 def _generate_recommendations( 

285 self, variables: list[EnvironmentVariable], result: EnvironmentValidationResult 

286 ) -> None: 

287 """Generate recommendations for environment variable configuration.""" 

288 var_names = {var.name for var in variables} 

289 

290 # Check for common missing variables 

291 common_vars = { 

292 "DATABASE_URL": "database connection", 

293 "SECRET_KEY": "application secret", 

294 "REDIS_URL": "Redis connection", 

295 "LOG_LEVEL": "logging configuration", 

296 "DEBUG": "debug mode flag", 

297 } 

298 

299 for var_name, description in common_vars.items(): 

300 if not any(var_name in name for name in var_names): 

301 result.recommendations.append( 

302 f"Consider adding {var_name} for {description}" 

303 ) 

304 

305 # Check for naming consistency 

306 prefixes = set() 

307 for var in variables: 

308 if "_" in var.name: 

309 prefix = var.name.split("_")[0] 

310 prefixes.add(prefix) 

311 

312 if len(prefixes) > 3: 

313 result.recommendations.append( 

314 "Consider using consistent prefixes for related variables" 

315 ) 

316 

317 def _generate_file_header(self, template: str | None) -> list[str]: 

318 """Generate .env file header with documentation.""" 

319 return [ 

320 "# FastBlocks Environment Configuration", 

321 "# Generated by FastBlocks Configuration Manager", 

322 f"# Template: {template or 'custom'}", 

323 "", 

324 "# IMPORTANT: This file contains sensitive information", 

325 "# - Do not commit this file to version control", 

326 "# - Copy to .env and customize for your environment", 

327 "# - Use .env.example for version control", 

328 "", 

329 ] 

330 

331 def _generate_variable_value( 

332 self, var: EnvironmentVariable, include_examples: bool 

333 ) -> str: 

334 """Generate value for an environment variable.""" 

335 if var.secret and include_examples: 

336 if var.value: 

337 return "***REDACTED***" 

338 return f"<your-{var.name.lower().replace('_', '-')}>" 

339 return var.value or var.default or "" 

340 

341 def _generate_variable_lines( 

342 self, var: EnvironmentVariable, include_docs: bool, include_examples: bool 

343 ) -> list[str]: 

344 """Generate lines for a single environment variable.""" 

345 lines = [] 

346 

347 # Add description 

348 if include_docs and var.description: 

349 lines.append(f"# {var.description}") 

350 

351 # Add requirement indicator 

352 if include_docs: 

353 requirement = "REQUIRED" if var.required else "OPTIONAL" 

354 lines.append(f"# {requirement}") 

355 

356 # Add validation info 

357 if include_docs and var.validator_pattern: 

358 lines.append(f"# Format: {var.validator_pattern}") 

359 

360 # Add variable with value 

361 value = self._generate_variable_value(var, include_examples) 

362 lines.extend((f"{var.name}={value}", "")) 

363 

364 return lines 

365 

366 def generate_environment_file( 

367 self, 

368 variables: list[EnvironmentVariable], 

369 output_path: Path | None = None, 

370 template: str | None = None, 

371 include_examples: bool = True, 

372 include_docs: bool = True, 

373 ) -> Path: 

374 """Generate comprehensive .env file.""" 

375 if not output_path: 

376 output_path = self.base_path / ".env" 

377 

378 lines = [] 

379 

380 if include_docs: 

381 lines.extend(self._generate_file_header(template)) 

382 

383 # Group variables by prefix 

384 grouped_vars = self._group_variables_by_prefix(variables) 

385 

386 for prefix, prefix_vars in grouped_vars.items(): 

387 if include_docs: 

388 lines.extend((f"# {prefix.upper()} Configuration", "")) 

389 

390 for var in prefix_vars: 

391 lines.extend( 

392 self._generate_variable_lines(var, include_docs, include_examples) 

393 ) 

394 

395 # Write file 

396 with output_path.open("w") as f: 

397 f.write("\n".join(lines)) 

398 

399 return output_path 

400 

401 def generate_environment_example( 

402 self, variables: list[EnvironmentVariable], output_path: Path | None = None 

403 ) -> Path: 

404 """Generate .env.example file for version control.""" 

405 if not output_path: 

406 output_path = self.base_path / ".env.example" 

407 

408 # Create example with placeholder values 

409 example_variables = [] 

410 for var in variables: 

411 example_var = EnvironmentVariable( 

412 name=var.name, 

413 value=self._generate_example_value(var), 

414 required=var.required, 

415 description=var.description, 

416 secret=var.secret, 

417 default=var.default, 

418 validator_pattern=var.validator_pattern, 

419 ) 

420 example_variables.append(example_var) 

421 

422 return self.generate_environment_file( 

423 example_variables, output_path, include_examples=True, include_docs=True 

424 ) 

425 

426 def _generate_example_value(self, var: EnvironmentVariable) -> str: 

427 """Generate example value for environment variable.""" 

428 name_lower = var.name.lower() 

429 

430 if var.secret: 

431 return f"your-{var.name.lower().replace('_', '-')}-here" 

432 

433 if "url" in name_lower: 

434 if "database" in name_lower: 

435 return "postgresql://user:password@localhost:5432/dbname" 

436 elif "redis" in name_lower: 

437 return "redis://localhost:6379/0" 

438 

439 return "https://example.com" 

440 

441 if "port" in name_lower: 

442 return "8000" 

443 

444 if "email" in name_lower: 

445 return "admin@example.com" 

446 

447 if "debug" in name_lower or name_lower.endswith("_flag"): 

448 return "false" 

449 

450 if "log" in name_lower and "level" in name_lower: 

451 return "INFO" 

452 

453 if "path" in name_lower: 

454 return "/path/to/directory" 

455 

456 return f"example-{var.name.lower().replace('_', '-')}" 

457 

458 def _group_variables_by_prefix( 

459 self, variables: list[EnvironmentVariable] 

460 ) -> dict[str, list[EnvironmentVariable]]: 

461 """Group variables by common prefix.""" 

462 grouped: dict[str, list[EnvironmentVariable]] = {} 

463 

464 for var in variables: 

465 # Determine prefix 

466 if "_" in var.name: 

467 prefix = var.name.split("_")[0] 

468 else: 

469 prefix = "general" 

470 

471 if prefix not in grouped: 

472 grouped[prefix] = [] 

473 grouped[prefix].append(var) 

474 

475 # Sort variables within each group 

476 for prefix in grouped: 

477 grouped[prefix].sort(key=lambda v: (not v.required, v.name)) 

478 

479 return grouped 

480 

481 def _parse_env_line(self, line: str) -> tuple[str, str] | None: 

482 """Parse a single environment variable line. 

483 

484 Args: 

485 line: Line to parse 

486 

487 Returns: 

488 Tuple of (key, value) or None if line should be skipped 

489 """ 

490 line = line.strip() 

491 

492 # Skip comments and empty lines 

493 if not line or line.startswith("#"): 

494 return None 

495 

496 # Parse variable assignment 

497 if "=" not in line: 

498 return None 

499 

500 key, value = line.split("=", 1) 

501 key = key.strip() 

502 value = value.strip() 

503 

504 # Remove quotes if present 

505 if (value.startswith('"') and value.endswith('"')) or ( 

506 value.startswith("'") and value.endswith("'") 

507 ): 

508 value = value[1:-1] 

509 

510 return key, value 

511 

512 def load_environment_from_file(self, env_file: Path) -> dict[str, str]: 

513 """Load environment variables from .env file.""" 

514 env_vars: dict[str, str] = {} 

515 

516 if not env_file.exists(): 

517 return env_vars 

518 

519 with env_file.open() as f: 

520 for line in f: 

521 parsed = self._parse_env_line(line) 

522 if parsed: 

523 key, value = parsed 

524 env_vars[key] = value 

525 

526 return env_vars 

527 

528 def sync_environment_variables( 

529 self, variables: list[EnvironmentVariable], env_file: Path | None = None 

530 ) -> dict[str, Any]: 

531 """Sync environment variables with .env file.""" 

532 env_file = env_file or (self.base_path / ".env") 

533 

534 # Load existing environment 

535 existing_env = ( 

536 self.load_environment_from_file(env_file) if env_file.exists() else {} 

537 ) 

538 

539 # Update variables with values from file 

540 updated_count = 0 

541 new_count = 0 

542 

543 for var in variables: 

544 if var.name in existing_env: 

545 if var.value != existing_env[var.name]: 

546 var.value = existing_env[var.name] 

547 updated_count += 1 

548 else: 

549 new_count += 1 

550 

551 return { 

552 "updated": updated_count, 

553 "new": new_count, 

554 "total": len(variables), 

555 "file_exists": env_file.exists() if env_file else False, 

556 } 

557 

558 def generate_environment_templates(self) -> dict[str, EnvironmentTemplate]: 

559 """Generate standard environment templates.""" 

560 templates = {} 

561 

562 # Development template 

563 dev_vars = [ 

564 EnvironmentVariable("DEBUG", "true", False, "Enable debug mode"), 

565 EnvironmentVariable("LOG_LEVEL", "DEBUG", False, "Logging level"), 

566 EnvironmentVariable( 

567 "SECRET_KEY", None, True, "Application secret key", True 

568 ), 

569 EnvironmentVariable( 

570 "DATABASE_URL", "sqlite:///./dev.db", False, "Database connection" 

571 ), 

572 EnvironmentVariable( 

573 "REDIS_URL", "redis://localhost:6379/0", False, "Redis connection" 

574 ), 

575 ] 

576 templates["development"] = EnvironmentTemplate( 

577 "development", "Development environment configuration", dev_vars 

578 ) 

579 

580 # Production template 

581 prod_vars = [ 

582 EnvironmentVariable("DEBUG", "false", True, "Debug mode (should be false)"), 

583 EnvironmentVariable("LOG_LEVEL", "WARNING", True, "Logging level"), 

584 EnvironmentVariable( 

585 "SECRET_KEY", None, True, "Application secret key", True 

586 ), 

587 EnvironmentVariable( 

588 "DATABASE_URL", None, True, "Database connection", True 

589 ), 

590 EnvironmentVariable("REDIS_URL", None, False, "Redis connection"), 

591 EnvironmentVariable("ALLOWED_HOSTS", "*", True, "Allowed hosts"), 

592 EnvironmentVariable("HTTPS_ONLY", "true", True, "Force HTTPS"), 

593 ] 

594 templates["production"] = EnvironmentTemplate( 

595 "production", "Production environment configuration", prod_vars 

596 ) 

597 

598 # Testing template 

599 test_vars = [ 

600 EnvironmentVariable("DEBUG", "true", True, "Enable debug mode"), 

601 EnvironmentVariable("LOG_LEVEL", "DEBUG", False, "Logging level"), 

602 EnvironmentVariable( 

603 "SECRET_KEY", 

604 "test-secret-key-do-not-use-in-production", 

605 True, 

606 "Test secret key", 

607 ), 

608 EnvironmentVariable( 

609 "DATABASE_URL", "sqlite:///:memory:", True, "In-memory test database" 

610 ), 

611 EnvironmentVariable("TESTING", "true", True, "Testing mode flag"), 

612 ] 

613 templates["testing"] = EnvironmentTemplate( 

614 "testing", "Testing environment configuration", test_vars 

615 ) 

616 

617 return templates 

618 

619 def extract_variables_from_configuration( 

620 self, config: ConfigurationSchema 

621 ) -> list[EnvironmentVariable]: 

622 """Extract all environment variables from configuration.""" 

623 all_variables = [] 

624 

625 # Add global environment variables 

626 all_variables.extend(config.global_environment) 

627 

628 # Add adapter environment variables 

629 for adapter_config in config.adapters.values(): 

630 if adapter_config.enabled: 

631 all_variables.extend(adapter_config.environment_variables) 

632 

633 # Remove duplicates (by name) 

634 seen_names = set() 

635 unique_variables = [] 

636 for var in all_variables: 

637 if var.name not in seen_names: 

638 unique_variables.append(var) 

639 seen_names.add(var.name) 

640 

641 return unique_variables 

642 

643 def audit_environment_security( 

644 self, variables: list[EnvironmentVariable] 

645 ) -> dict[str, list[str]]: 

646 """Perform security audit of environment variables.""" 

647 audit_results: dict[str, Any] = { 

648 "critical": [], 

649 "high": [], 

650 "medium": [], 

651 "low": [], 

652 "info": [], 

653 } 

654 

655 for var in variables: 

656 self._audit_secret_marking(var, audit_results) 

657 self._audit_secret_strength(var, audit_results) 

658 self._audit_required_values(var, audit_results) 

659 self._audit_format_validation(var, audit_results) 

660 self._audit_best_practices(var, audit_results) 

661 

662 return audit_results 

663 

664 def _audit_secret_marking( 

665 self, var: EnvironmentVariable, audit_results: dict[str, list[str]] 

666 ) -> None: 

667 """Audit if secrets are properly marked.""" 

668 is_secret_name = any( 

669 pattern.match(var.name) for pattern in self.secret_patterns 

670 ) 

671 if is_secret_name and not var.secret: 

672 audit_results["critical"].append( 

673 f"{var.name}: Contains sensitive data but not marked as secret" 

674 ) 

675 

676 def _audit_secret_strength( 

677 self, var: EnvironmentVariable, audit_results: dict[str, list[str]] 

678 ) -> None: 

679 """Audit secret value strength.""" 

680 if var.secret and var.value: 

681 if len(var.value) < 16: 

682 audit_results["high"].append( 

683 f"{var.name}: Secret is too short (< 16 characters)" 

684 ) 

685 if var.value.lower() in ("password", "secret", "admin", "test"): 

686 audit_results["high"].append( 

687 f"{var.name}: Using weak/common secret value" 

688 ) 

689 

690 def _audit_required_values( 

691 self, var: EnvironmentVariable, audit_results: dict[str, list[str]] 

692 ) -> None: 

693 """Audit if required variables have values.""" 

694 if var.required and not var.value and not var.default: 

695 audit_results["medium"].append( 

696 f"{var.name}: Required variable has no value or default" 

697 ) 

698 

699 def _audit_format_validation( 

700 self, var: EnvironmentVariable, audit_results: dict[str, list[str]] 

701 ) -> None: 

702 """Audit format validation patterns.""" 

703 if var.validator_pattern and var.value: 

704 if not re.match( 

705 var.validator_pattern, var.value 

706 ): # REGEX OK: custom validator pattern from config 

707 audit_results["low"].append( 

708 f"{var.name}: Value doesn't match expected pattern" 

709 ) 

710 

711 def _audit_best_practices( 

712 self, var: EnvironmentVariable, audit_results: dict[str, list[str]] 

713 ) -> None: 

714 """Audit best practice compliance.""" 

715 if not var.description: 

716 audit_results["info"].append(f"{var.name}: Missing description")