captive-stack-detector 0.8.3 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 272eb63b46430d6bf1d609c667e0e6c33a8a103120b0e7d12942989d196c34a8
4
- data.tar.gz: fdcbd149dd558c16d126f7378a88f7318224a6f19e0561c9949765b8266f3ff6
3
+ metadata.gz: 9b3af49e63482068e44d5c1b1064695ac49eadd05c1a63447e29cfe57423492b
4
+ data.tar.gz: ef69968ebaec94b711a248afe56dad7bfcbc0b8fca3f349683810375d3929ff9
5
5
  SHA512:
6
- metadata.gz: 55c86da390fd33c90773cdb0a920cfa867f2182dc8c992fc971f76b7fdcee68c4f33f686eff186130d500db2af48a61c954975067775c436e6711df45c464f32
7
- data.tar.gz: 5ef3670152f0c1807d8eb479ff931b5992657728cb09f73d85f464164fe491b489fc4f504c180d384b2d7dc48b6889c03fdedab1cc1f85672edaf78edd9196c0
6
+ metadata.gz: 88acfedc7245af2d7526bc4416159d137f4b0fdd3649f883a7cc8d8f41af9bef4ef32f8c26c67921314d86b8d7b47437cd1bb01b1459a0cfa52e341b47e4840c
7
+ data.tar.gz: f9d7277815801c5e0e0b769dc68d0b884d911d88044318700b4af975957f31bbd4e43eda31c7089f55ebdbf4f3ef948e877a96bf4e5b8cbc0a48c75550abba10
data/docs/stacks.md CHANGED
@@ -69,6 +69,30 @@ Pour Astro, utilisez l'adaptateur `@astrojs/node` en mode `standalone`, avec `ou
69
69
  - Une ligne `worker: <commande>` dans un `Procfile` lance un worker.
70
70
 
71
71
 
72
+ ### Variables à renseigner
73
+
74
+ La détection Node recherche les lectures à clé littérale `process.env.MA_CLE`,
75
+ `process.env["MA_CLE"]`, `import.meta.env.MA_CLE` et `import.meta.env["MA_CLE"]`
76
+ dans les sources JavaScript, TypeScript, JSX, TSX, Astro, Vue et Svelte, y compris les
77
+ fichiers de configuration à la racine. Les imports nommés de `astro:env/client` et
78
+ `astro:env/server` sont également reconnus.
79
+
80
+ Les modèles `.env.example`, `.env.sample` et `.env.template` à la racine déclarent aussi
81
+ des noms de variables à renseigner. **Leurs valeurs ne sont jamais reprises** : chaque nom
82
+ est signalé avec la valeur `placeholder`. Les fichiers `.env`, `.env.local` et les autres
83
+ fichiers d'environnement réels ne sont pas lus.
84
+
85
+ Les commentaires, chaînes d'exemple, dépendances, sorties de build, scripts d'outillage
86
+ (`scripts/`, `bin/`), documentation, exemples, tests, stories et
87
+ déclarations TypeScript sont ignorés. Les constantes intégrées de Vite et les variables
88
+ `NODE_ENV`, `NODE_OPTIONS`, `HOST` et `PORT` ne sont pas demandées. Une lecture avec une
89
+ valeur de repli littérale (`?? "local"`, `|| "local"`, nombre ou booléen) est facultative ;
90
+ une lecture sans repli ailleurs, ou sa déclaration dans un modèle, demande toujours la clé.
91
+
92
+ L'analyse est statique : les clés calculées, la déstructuration et les appels indirects
93
+ (comme `getSecret(nom)`) ne sont pas déduits. Déclarez ces clés dans un modèle si nécessaire.
94
+ La même analyse est utilisée sur les dépôts locaux et GitHub.
95
+
72
96
  ### Variables exposées au navigateur
73
97
 
74
98
  Une variable n'arrive dans le code du navigateur que si elle porte le préfixe de votre bundler. Captive le déduit de vos dépendances :
@@ -77,6 +101,7 @@ Une variable n'arrive dans le code du navigateur que si elle porte le préfixe d
77
101
  | --- | --- |
78
102
  | `expo` | `EXPO_PUBLIC_` |
79
103
  | `next` | `NEXT_PUBLIC_` |
104
+ | `astro` | `PUBLIC_` |
80
105
  | `vite` | `VITE_` |
81
106
 
82
107
  Si plusieurs sont présentes, c'est le premier de la liste qui l'emporte (`vite` est souvent là pour les tests seulement).
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveStackDetector
4
+ # Les modèles déclarent des noms ; leurs valeurs d'exemple ne sont jamais importées.
5
+ module EnvTemplateScanner
6
+ def self.scan(content)
7
+ content.scan(/^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=/).flatten.to_h { |key| [ key, "placeholder" ] }
8
+ end
9
+ end
10
+ end
@@ -30,7 +30,7 @@ module CaptiveStackDetector
30
30
  end
31
31
 
32
32
  def list(dir)
33
- Dir.glob("#{dir}/**/*", base: @path).select { |entry| File.file?(File.join(@path, entry)) }.sort
33
+ Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH, base: @path).select { |entry| File.file?(File.join(@path, entry)) }.sort
34
34
  end
35
35
  end
36
36
 
@@ -9,7 +9,7 @@ module CaptiveStackDetector
9
9
  end
10
10
 
11
11
  def files_in(dir)
12
- prefix = "#{dir}/"
12
+ prefix = dir == "." ? "" : "#{dir}/"
13
13
  @entries.filter_map { |entry| entry["path"] if entry["type"] == "blob" && entry["path"].start_with?(prefix) }
14
14
  end
15
15
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "js_env_files"
4
+
5
+ module CaptiveStackDetector
6
+ # Ne lit que les sources et les modèles d'environnement, jamais les secrets .env.
7
+ class JsEnvDetector
8
+ def initialize(reader)
9
+ @reader = reader
10
+ end
11
+
12
+ def detect
13
+ vars = @reader.list(".").sort.each_with_object({}) do |entry, found|
14
+ path = entry.delete_prefix("./")
15
+ parser = JsEnvFiles.parser(path)
16
+ found.merge!(parser.scan(@reader.read(path).to_s)) if parser
17
+ end
18
+ vars.reject { |key, _| JsEnvScanner::RUNTIME_VARS.include?(key) }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "js_env_scanner"
4
+ require_relative "env_template_scanner"
5
+
6
+ module CaptiveStackDetector
7
+ # Choisit les fichiers lisibles avant tout téléchargement de contenu.
8
+ module JsEnvFiles
9
+ TEMPLATES = %w[.env.example .env.sample .env.template].freeze
10
+ EXCLUDED = %r{
11
+ (?:\A|/)(?:node_modules|vendor|\.git|dist|build|\.output|\.next|\.nuxt|\.astro|coverage|
12
+ test|tests|spec|__tests__|__mocks__|\.storybook|\.github|scripts|bin|docs|examples|fixtures|e2e)(?:/|\z)
13
+ }x
14
+ SOURCE = /(?<!\.test)(?<!\.spec)(?<!\.stories)(?<!\.d)\.(?:[cm]?[jt]sx?|astro|vue|svelte)\z/
15
+
16
+ def self.parser(path)
17
+ return nil if EXCLUDED.match?(path)
18
+ return EnvTemplateScanner if TEMPLATES.include?(path)
19
+
20
+ JsEnvScanner if SOURCE.match?(path)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+ require_relative "js_env_syntax"
5
+
6
+ module CaptiveStackDetector
7
+ # Énumère les accès littéraux en ignorant commentaires et texte non exécuté.
8
+ class JsEnvLexer
9
+ include JsEnvSyntax
10
+
11
+ def initialize(content)
12
+ @scanner = StringScanner.new(content, fixed_anchor: true)
13
+ @references = []
14
+ end
15
+
16
+ def references
17
+ code
18
+ @references
19
+ end
20
+
21
+ private
22
+
23
+ def code(terminator = nil)
24
+ until @scanner.eos?
25
+ next if @scanner.skip(IGNORED)
26
+
27
+ break if consume == terminator
28
+ end
29
+ end
30
+
31
+ def consume
32
+ if @scanner.scan(ASTRO_IMPORT)
33
+ astro_import(@scanner[1])
34
+ elsif @scanner.scan(REFERENCE)
35
+ reference
36
+ else
37
+ punctuation(@scanner.getch)
38
+ end
39
+ end
40
+
41
+ def punctuation(char)
42
+ code("}") if char == "{"
43
+ template if char == "`"
44
+ char
45
+ end
46
+
47
+ def astro_import(names)
48
+ JsEnvSyntax.astro_keys(names).each { |key| @references << [ key, "astro", "" ] }
49
+ end
50
+
51
+ def reference
52
+ scope = @scanner[1].start_with?("import") ? "vite" : "node"
53
+ @references << [ @scanner[2] || @scanner[3], scope, @scanner.rest ]
54
+ end
55
+
56
+ def template
57
+ until @scanner.eos? || @scanner.skip(/`/)
58
+ code("}") if @scanner.skip(/\$\{/)
59
+ @scanner.skip(/(?:\\.|[^`$]|\$(?!\{))+/m)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "js_env_lexer"
4
+
5
+ module CaptiveStackDetector
6
+ # Ne demande que les lectures sans valeur de repli et hors variables intégrées.
7
+ module JsEnvScanner
8
+ DEFAULT = /\A\s*(?:\?\?|\|\|)\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|true\b|false\b|\d+(?:\.\d+)?\b)/
9
+ ASSIGNMENT = /\A\s*(?:=(?!=|>)|\?\?=|\|\|=|&&=)/
10
+ RUNTIME_VARS = %w[NODE_ENV NODE_OPTIONS PORT HOST].freeze
11
+ VITE_VARS = %w[BASE_URL MODE DEV PROD SSR].freeze
12
+
13
+ def self.scan(content)
14
+ JsEnvLexer.new(content).references.each_with_object({}) do |(key, scope, rest), found|
15
+ next if RUNTIME_VARS.include?(key) || (scope == "vite" && VITE_VARS.include?(key))
16
+ next if DEFAULT.match?(rest) || ASSIGNMENT.match?(rest)
17
+
18
+ found[key] = "placeholder"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveStackDetector
4
+ # Tokens reconnus sans interpréter le programme JavaScript.
5
+ module JsEnvSyntax
6
+ KEY = /[A-Z_][A-Z0-9_]*/
7
+ REFERENCE = /
8
+ (?<![\w$.])(process\s*\.\s*env|import\s*\.\s*meta\s*\.\s*env)
9
+ (?:\s*\.\s*(#{KEY})(?![\w$])|\s*\[\s*['"](#{KEY})['"]\s*\])
10
+ /x
11
+ ASTRO_IMPORT = /import\s*\{([^}]+)\}\s*from\s*['"]astro:env\/(?:client|server)['"]/m
12
+ COMMENT = %r{//[^\n]*|/\*.*?\*/|<!--.*?-->}m
13
+ IGNORED = Regexp.union(COMMENT, /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/m)
14
+ def self.astro_keys(names)
15
+ names.gsub(COMMENT, "").split(",").filter_map { |name| name.strip[/\A#{KEY}\b/] }
16
+ end
17
+ end
18
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../captive_stack_detector"
4
4
  require_relative "package_json_analyzer"
5
+ require_relative "js_env_detector"
5
6
 
6
7
  module CaptiveStackDetector
7
8
  class JsStackDetector
@@ -20,7 +21,7 @@ module CaptiveStackDetector
20
21
  services: Services.new(database: @analyzer.database, queue: @analyzer.queue, backend: @analyzer.backend),
21
22
  worker: build_worker,
22
23
  runtime: Runtime.new(ruby: nil, node: @reader.node_version),
23
- env_vars: {},
24
+ env_vars: JsEnvDetector.new(@reader).detect,
24
25
  system_packages: [],
25
26
  public_env_prefix: @analyzer.public_env_prefix,
26
27
  )
@@ -5,7 +5,7 @@ require_relative "ssr_framework_detector"
5
5
 
6
6
  module CaptiveStackDetector
7
7
  class PackageJsonAnalyzer
8
- PUBLIC_ENV_PREFIXES = { "expo" => "EXPO_PUBLIC_", "next" => "NEXT_PUBLIC_", "vite" => "VITE_" }.freeze
8
+ PUBLIC_ENV_PREFIXES = { "expo" => "EXPO_PUBLIC_", "next" => "NEXT_PUBLIC_", "astro" => "PUBLIC_", "vite" => "VITE_" }.freeze
9
9
  # Des clients, pas des ORM : Prisma ou Drizzle parlent aussi MySQL et SQLite, leur
10
10
  # présence ne dit pas que l'app attend une base Postgres.
11
11
  POSTGRES_CLIENTS = %w[pg postgres].freeze
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CaptiveStackDetector
4
- VERSION = "0.8.3"
4
+ VERSION = "0.8.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: captive-stack-detector
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.8.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Captive Studio
@@ -21,6 +21,7 @@ files:
21
21
  - lib/captive/stack/detector.rb
22
22
  - lib/captive_stack_detector.rb
23
23
  - lib/captive_stack_detector/aptfile_parser.rb
24
+ - lib/captive_stack_detector/env_template_scanner.rb
24
25
  - lib/captive_stack_detector/env_vars_scanner.rb
25
26
  - lib/captive_stack_detector/file_content_parser.rb
26
27
  - lib/captive_stack_detector/file_reader.rb
@@ -28,6 +29,11 @@ files:
28
29
  - lib/captive_stack_detector/gemfile_analyzer.rb
29
30
  - lib/captive_stack_detector/github_api_client.rb
30
31
  - lib/captive_stack_detector/github_tree.rb
32
+ - lib/captive_stack_detector/js_env_detector.rb
33
+ - lib/captive_stack_detector/js_env_files.rb
34
+ - lib/captive_stack_detector/js_env_lexer.rb
35
+ - lib/captive_stack_detector/js_env_scanner.rb
36
+ - lib/captive_stack_detector/js_env_syntax.rb
31
37
  - lib/captive_stack_detector/js_stack_detector.rb
32
38
  - lib/captive_stack_detector/node_version_detector.rb
33
39
  - lib/captive_stack_detector/package_json_analyzer.rb