1
0

config-secret-redaction.test.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. /**
  2. * #383 — CodeGraph indexes config KEYS but must never surface config VALUES.
  3. *
  4. * Spring `application.{yml,properties}` keys are indexed as `constant` nodes so
  5. * `@Value` resolution works, but their values are routinely secrets (DB
  6. * passwords, API keys, JDBC URLs with embedded creds). CodeGraph must surface
  7. * the KEY and never the value — not in node metadata (docstring/signature),
  8. * not via `codegraph_explore`'s verbatim source dump, and not via
  9. * `codegraph_node` `includeCode`. An agent that genuinely needs a value can
  10. * read the file itself (a deliberate pull); CodeGraph must never volunteer it.
  11. */
  12. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  13. import * as fs from 'fs';
  14. import * as path from 'path';
  15. import * as os from 'os';
  16. import CodeGraph from '../src/index';
  17. import { ToolHandler } from '../src/mcp/tools';
  18. const SECRET = 'sk-live-DO-NOT-LEAK-2f9a4c7e1b';
  19. describe('config secret redaction (#383)', () => {
  20. let tmpDir: string;
  21. let cg: CodeGraph;
  22. let handler: ToolHandler;
  23. beforeEach(async () => {
  24. tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-config-secret-'));
  25. const javaDir = path.join(tmpDir, 'src/main/java/com/example');
  26. const resDir = path.join(tmpDir, 'src/main/resources');
  27. fs.mkdirSync(javaDir, { recursive: true });
  28. fs.mkdirSync(resDir, { recursive: true });
  29. // pom.xml triggers Spring detection so the resolver parses the config files.
  30. fs.writeFileSync(
  31. path.join(tmpDir, 'pom.xml'),
  32. '<project><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency></dependencies></project>\n',
  33. );
  34. fs.writeFileSync(
  35. path.join(resDir, 'application.properties'),
  36. `server.port=8080\nspring.datasource.password=${SECRET}\n`,
  37. );
  38. fs.writeFileSync(
  39. path.join(resDir, 'application.yml'),
  40. `app:\n api:\n key: "${SECRET}"\n`,
  41. );
  42. fs.writeFileSync(
  43. path.join(javaDir, 'DataConfig.java'),
  44. 'package com.example;\n' +
  45. 'import org.springframework.beans.factory.annotation.Value;\n' +
  46. 'public class DataConfig {\n' +
  47. ' @Value("${spring.datasource.password}") private String dbPass;\n' +
  48. ' @Value("${app.api.key}") private String apiKey;\n' +
  49. '}\n',
  50. );
  51. cg = CodeGraph.initSync(tmpDir);
  52. await cg.indexAll();
  53. handler = new ToolHandler(cg);
  54. });
  55. afterEach(() => {
  56. if (cg) cg.destroy();
  57. if (fs.existsSync(tmpDir)) fs.rmSync(tmpDir, { recursive: true, force: true });
  58. });
  59. const configKeys = () =>
  60. cg.getNodesByKind('constant').filter((n) => n.language === 'yaml' || n.language === 'properties');
  61. it('still indexes config KEYS as nodes (resolution must not regress)', () => {
  62. const byQn = (qn: string) => configKeys().find((n) => n.qualifiedName === qn);
  63. expect(byQn('spring.datasource.password'), '.properties key indexed').toBeDefined();
  64. expect(byQn('app.api.key'), 'yaml key indexed').toBeDefined();
  65. });
  66. it('never stores the secret VALUE in node metadata (docstring/signature/name)', () => {
  67. const keys = configKeys();
  68. expect(keys.length).toBeGreaterThan(0);
  69. for (const n of keys) {
  70. expect(n.docstring ?? '', `docstring of ${n.qualifiedName}`).not.toContain(SECRET);
  71. expect(n.signature ?? '', `signature of ${n.qualifiedName}`).not.toContain(SECRET);
  72. expect(n.name, `name of ${n.qualifiedName}`).not.toContain(SECRET);
  73. }
  74. });
  75. it('codegraph_explore surfaces the config key but NEVER the secret value', async () => {
  76. const res = await handler.execute('codegraph_explore', {
  77. query: 'DataConfig dbPass apiKey spring.datasource.password app.api.key',
  78. });
  79. const text = res.content.map((c) => c.text).join('\n');
  80. expect(text).toContain('password'); // the key is in scope (non-vacuous)
  81. expect(text).not.toContain(SECRET); // ...but the value is never dumped
  82. });
  83. it('codegraph_node includeCode returns the key, not the secret value', async () => {
  84. const res = await handler.execute('codegraph_node', {
  85. symbol: 'spring.datasource.password',
  86. includeCode: true,
  87. });
  88. const text = res.content.map((c) => c.text).join('\n');
  89. expect(text).toContain('password'); // found the node
  90. expect(text).not.toContain(SECRET); // value redacted from the code path
  91. });
  92. });