gea-program 0.55.0__tar.gz → 0.57.0__tar.gz

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.
Files changed (368) hide show
  1. {gea_program-0.55.0/gea_program.egg-info → gea_program-0.57.0}/PKG-INFO +35 -2
  2. {gea_program-0.55.0 → gea_program-0.57.0}/README.md +34 -1
  3. {gea_program-0.55.0 → gea_program-0.57.0}/gea/__init__.py +1 -1
  4. {gea_program-0.55.0 → gea_program-0.57.0}/gea/__main__.py +74 -0
  5. {gea_program-0.55.0 → gea_program-0.57.0}/gea/acceptance_tests.py +362 -0
  6. {gea_program-0.55.0 → gea_program-0.57.0}/gea/cli.py +14 -1
  7. {gea_program-0.55.0 → gea_program-0.57.0}/gea/doctor.py +1 -1
  8. gea_program-0.57.0/gea/help/IT_SECURITY.md +138 -0
  9. gea_program-0.57.0/gea/help/company-sign-in.md +19 -0
  10. {gea_program-0.55.0 → gea_program-0.57.0}/gea/helplib.py +9 -0
  11. gea_program-0.57.0/gea/oidc.py +379 -0
  12. gea_program-0.57.0/gea/oidc_mock.py +135 -0
  13. {gea_program-0.55.0 → gea_program-0.57.0}/gea/service.py +122 -4
  14. {gea_program-0.55.0 → gea_program-0.57.0}/gea/web/app.html +21 -5
  15. {gea_program-0.55.0 → gea_program-0.57.0/gea_program.egg-info}/PKG-INFO +35 -2
  16. {gea_program-0.55.0 → gea_program-0.57.0}/gea_program.egg-info/SOURCES.txt +4 -0
  17. {gea_program-0.55.0 → gea_program-0.57.0}/pyproject.toml +1 -1
  18. {gea_program-0.55.0 → gea_program-0.57.0}/LICENSE +0 -0
  19. {gea_program-0.55.0 → gea_program-0.57.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  20. {gea_program-0.55.0 → gea_program-0.57.0}/gea/accuracy_statement.py +0 -0
  21. {gea_program-0.55.0 → gea_program-0.57.0}/gea/alarm_engine.py +0 -0
  22. {gea_program-0.55.0 → gea_program-0.57.0}/gea/alerts.py +0 -0
  23. {gea_program-0.55.0 → gea_program-0.57.0}/gea/backup.py +0 -0
  24. {gea_program-0.55.0 → gea_program-0.57.0}/gea/bench.py +0 -0
  25. {gea_program-0.55.0 → gea_program-0.57.0}/gea/blind_harness.py +0 -0
  26. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  27. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  28. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  29. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  30. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  31. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  32. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  33. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  34. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  35. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  36. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  37. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  38. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  39. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  40. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  41. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  42. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  43. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  44. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  45. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  46. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  47. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  48. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  49. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  50. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  51. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  52. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  53. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  54. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  55. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  56. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  57. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  58. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  59. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  60. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  61. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  62. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  63. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  64. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  65. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  66. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  67. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  68. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  69. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  70. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  71. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  72. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  73. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  74. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  75. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  76. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  77. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  78. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  79. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  80. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  81. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  82. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  83. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  84. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  85. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  86. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  87. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  88. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  89. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  90. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  91. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  92. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  93. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/med_160_sapropels.txt +0 -0
  94. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  95. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  96. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  97. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  98. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  99. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  100. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  101. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  102. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  103. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  104. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  105. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  106. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  107. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  108. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  109. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  110. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  111. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  112. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  113. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  114. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  115. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  116. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  117. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  118. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  119. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  120. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  121. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  122. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  123. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  124. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  125. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  126. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  127. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  128. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  129. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  130. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog_map.py +0 -0
  131. {gea_program-0.55.0 → gea_program-0.57.0}/gea/catalog_register.py +0 -0
  132. {gea_program-0.55.0 → gea_program-0.57.0}/gea/certificates.py +0 -0
  133. {gea_program-0.55.0 → gea_program-0.57.0}/gea/client_reports.py +0 -0
  134. {gea_program-0.55.0 → gea_program-0.57.0}/gea/collision.py +0 -0
  135. {gea_program-0.55.0 → gea_program-0.57.0}/gea/config_versioning.py +0 -0
  136. {gea_program-0.55.0 → gea_program-0.57.0}/gea/conformance.py +0 -0
  137. {gea_program-0.55.0 → gea_program-0.57.0}/gea/control_room.py +0 -0
  138. {gea_program-0.55.0 → gea_program-0.57.0}/gea/correlation.py +0 -0
  139. {gea_program-0.55.0 → gea_program-0.57.0}/gea/dashboard.py +0 -0
  140. {gea_program-0.55.0 → gea_program-0.57.0}/gea/declarations.py +0 -0
  141. {gea_program-0.55.0 → gea_program-0.57.0}/gea/decline.py +0 -0
  142. {gea_program-0.55.0 → gea_program-0.57.0}/gea/deviation.py +0 -0
  143. {gea_program-0.55.0 → gea_program-0.57.0}/gea/dlis.py +0 -0
  144. {gea_program-0.55.0 → gea_program-0.57.0}/gea/dnp3.py +0 -0
  145. {gea_program-0.55.0 → gea_program-0.57.0}/gea/dossier.py +0 -0
  146. {gea_program-0.55.0 → gea_program-0.57.0}/gea/dossier_verify.py +0 -0
  147. {gea_program-0.55.0 → gea_program-0.57.0}/gea/downhole_engine.py +0 -0
  148. {gea_program-0.55.0 → gea_program-0.57.0}/gea/drift_monitor.py +0 -0
  149. {gea_program-0.55.0 → gea_program-0.57.0}/gea/drill_string.py +0 -0
  150. {gea_program-0.55.0 → gea_program-0.57.0}/gea/earth_model.py +0 -0
  151. {gea_program-0.55.0 → gea_program-0.57.0}/gea/economics.py +0 -0
  152. {gea_program-0.55.0 → gea_program-0.57.0}/gea/ed25519.py +0 -0
  153. {gea_program-0.55.0 → gea_program-0.57.0}/gea/edr.py +0 -0
  154. {gea_program-0.55.0 → gea_program-0.57.0}/gea/elevation.py +0 -0
  155. {gea_program-0.55.0 → gea_program-0.57.0}/gea/etp.py +0 -0
  156. {gea_program-0.55.0 → gea_program-0.57.0}/gea/etp12_schemas.json +0 -0
  157. {gea_program-0.55.0 → gea_program-0.57.0}/gea/example_register_map.json +0 -0
  158. {gea_program-0.55.0 → gea_program-0.57.0}/gea/fat_sat.py +0 -0
  159. {gea_program-0.55.0 → gea_program-0.57.0}/gea/files.py +0 -0
  160. {gea_program-0.55.0 → gea_program-0.57.0}/gea/fixtures/README.md +0 -0
  161. {gea_program-0.55.0 → gea_program-0.57.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  162. {gea_program-0.55.0 → gea_program-0.57.0}/gea/fixtures/comcat_mahoning_1990-2026.txt +0 -0
  163. {gea_program-0.55.0 → gea_program-0.57.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  164. {gea_program-0.55.0 → gea_program-0.57.0}/gea/fixtures/ne_110m_coastline.json +0 -0
  165. {gea_program-0.55.0 → gea_program-0.57.0}/gea/follower.py +0 -0
  166. {gea_program-0.55.0 → gea_program-0.57.0}/gea/forward_model.py +0 -0
  167. {gea_program-0.55.0 → gea_program-0.57.0}/gea/frac_hits.py +0 -0
  168. {gea_program-0.55.0 → gea_program-0.57.0}/gea/friction_priors.py +0 -0
  169. {gea_program-0.55.0 → gea_program-0.57.0}/gea/gamma.py +0 -0
  170. {gea_program-0.55.0 → gea_program-0.57.0}/gea/gauge_aging.py +0 -0
  171. {gea_program-0.55.0 → gea_program-0.57.0}/gea/gauge_specs.py +0 -0
  172. {gea_program-0.55.0 → gea_program-0.57.0}/gea/geodesy.py +0 -0
  173. {gea_program-0.55.0 → gea_program-0.57.0}/gea/geomag.py +0 -0
  174. {gea_program-0.55.0 → gea_program-0.57.0}/gea/gravity_reference.py +0 -0
  175. {gea_program-0.55.0 → gea_program-0.57.0}/gea/hartip.py +0 -0
  176. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/TESTER_GUIDE.md +0 -0
  177. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/alarms.md +0 -0
  178. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/alerts.md +0 -0
  179. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/audit-update.md +0 -0
  180. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/backup.md +0 -0
  181. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/catalog.md +0 -0
  182. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/charts.md +0 -0
  183. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/collision.md +0 -0
  184. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/conformance.md +0 -0
  185. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/control-room.md +0 -0
  186. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/data-in.md +0 -0
  187. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/declare.md +0 -0
  188. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/decline.md +0 -0
  189. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/dlis.md +0 -0
  190. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/doctor.md +0 -0
  191. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/dossier.md +0 -0
  192. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/drift.md +0 -0
  193. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/drillstring.md +0 -0
  194. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/economics.md +0 -0
  195. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/edr.md +0 -0
  196. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/etp.md +0 -0
  197. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/field-ports.md +0 -0
  198. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/files.md +0 -0
  199. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/frac-hits.md +0 -0
  200. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/historian.md +0 -0
  201. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/https.md +0 -0
  202. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/instruments.md +0 -0
  203. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/interval.md +0 -0
  204. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/invoice-check.md +0 -0
  205. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/map.md +0 -0
  206. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/month-end.md +0 -0
  207. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/notifications.md +0 -0
  208. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/opseis.md +0 -0
  209. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/osdu.md +0 -0
  210. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/pad-summary.md +0 -0
  211. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/patches.md +0 -0
  212. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/penetrator.md +0 -0
  213. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/plan.md +0 -0
  214. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/pore-pressure.md +0 -0
  215. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/ppdm.md +0 -0
  216. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/quakeml.md +0 -0
  217. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/quality.md +0 -0
  218. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/report-history.md +0 -0
  219. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/rig-time.md +0 -0
  220. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/seedlink.md +0 -0
  221. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/seismic.md +0 -0
  222. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/setup.md +0 -0
  223. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/simulation.md +0 -0
  224. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/site.md +0 -0
  225. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/sites.md +0 -0
  226. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/slip.md +0 -0
  227. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/soak.md +0 -0
  228. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/sra.md +0 -0
  229. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/start.md +0 -0
  230. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/structure.md +0 -0
  231. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/tools.md +0 -0
  232. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/torque-drag.md +0 -0
  233. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/transients.md +0 -0
  234. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/upkeep.md +0 -0
  235. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/vibration.md +0 -0
  236. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/well-report.md +0 -0
  237. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/well-tests.md +0 -0
  238. {gea_program-0.55.0 → gea_program-0.57.0}/gea/help/well3d.md +0 -0
  239. {gea_program-0.55.0 → gea_program-0.57.0}/gea/housekeeping.py +0 -0
  240. {gea_program-0.55.0 → gea_program-0.57.0}/gea/interval.py +0 -0
  241. {gea_program-0.55.0 → gea_program-0.57.0}/gea/inverse_engine.py +0 -0
  242. {gea_program-0.55.0 → gea_program-0.57.0}/gea/invoice_check.py +0 -0
  243. {gea_program-0.55.0 → gea_program-0.57.0}/gea/jobs.py +0 -0
  244. {gea_program-0.55.0 → gea_program-0.57.0}/gea/live_ports.py +0 -0
  245. {gea_program-0.55.0 → gea_program-0.57.0}/gea/loadtest.py +0 -0
  246. {gea_program-0.55.0 → gea_program-0.57.0}/gea/matplotlib_demo.py +0 -0
  247. {gea_program-0.55.0 → gea_program-0.57.0}/gea/modbus.py +0 -0
  248. {gea_program-0.55.0 → gea_program-0.57.0}/gea/modbus_rtu.py +0 -0
  249. {gea_program-0.55.0 → gea_program-0.57.0}/gea/model_card.py +0 -0
  250. {gea_program-0.55.0 → gea_program-0.57.0}/gea/month_end.py +0 -0
  251. {gea_program-0.55.0 → gea_program-0.57.0}/gea/mqtt_port.py +0 -0
  252. {gea_program-0.55.0 → gea_program-0.57.0}/gea/notify.py +0 -0
  253. {gea_program-0.55.0 → gea_program-0.57.0}/gea/op_seismicity.py +0 -0
  254. {gea_program-0.55.0 → gea_program-0.57.0}/gea/opcua_port.py +0 -0
  255. {gea_program-0.55.0 → gea_program-0.57.0}/gea/operator_app.py +0 -0
  256. {gea_program-0.55.0 → gea_program-0.57.0}/gea/osdu.py +0 -0
  257. {gea_program-0.55.0 → gea_program-0.57.0}/gea/pad_summary.py +0 -0
  258. {gea_program-0.55.0 → gea_program-0.57.0}/gea/patches.py +0 -0
  259. {gea_program-0.55.0 → gea_program-0.57.0}/gea/pdf_report.py +0 -0
  260. {gea_program-0.55.0 → gea_program-0.57.0}/gea/penetrator.py +0 -0
  261. {gea_program-0.55.0 → gea_program-0.57.0}/gea/permits.py +0 -0
  262. {gea_program-0.55.0 → gea_program-0.57.0}/gea/piwebapi.py +0 -0
  263. {gea_program-0.55.0 → gea_program-0.57.0}/gea/pore_pressure.py +0 -0
  264. {gea_program-0.55.0 → gea_program-0.57.0}/gea/ports.py +0 -0
  265. {gea_program-0.55.0 → gea_program-0.57.0}/gea/ppdm.py +0 -0
  266. {gea_program-0.55.0 → gea_program-0.57.0}/gea/profile_catalog.py +0 -0
  267. {gea_program-0.55.0 → gea_program-0.57.0}/gea/project.py +0 -0
  268. {gea_program-0.55.0 → gea_program-0.57.0}/gea/qt6_downhole_app.py +0 -0
  269. {gea_program-0.55.0 → gea_program-0.57.0}/gea/quakeml.py +0 -0
  270. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reconciler.py +0 -0
  271. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  272. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  273. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/WMM2025.COF +0 -0
  274. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
  275. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/eia_prices.provenance.json +0 -0
  276. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/eia_wti_daily.csv.gz +0 -0
  277. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/iscwsa_rev4_example.json +0 -0
  278. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/iscwsa_rev5_example.json +0 -0
  279. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/l0701_sonic_gr.las.gz +0 -0
  280. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/l0701_sonic_gr.provenance.json +0 -0
  281. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  282. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  283. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  284. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  285. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/mahoning_example.json +0 -0
  286. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/mud_motor_specs.json +0 -0
  287. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/owsg_tool_models.json +0 -0
  288. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/penetrator_ratings.json +0 -0
  289. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/seismicity_protocols.json +0 -0
  290. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/tool_sources.json +0 -0
  291. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/us_outline.json +0 -0
  292. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/us_outline.provenance.json +0 -0
  293. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_3d.provenance.json +0 -0
  294. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_f4_interpretation.las.gz +0 -0
  295. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_f4_logs.provenance.json +0 -0
  296. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_f4_lwd.las.gz +0 -0
  297. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_surfaces.json.gz +0 -0
  298. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_traj_F-10.xml.gz +0 -0
  299. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_traj_F-4.xml.gz +0 -0
  300. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  301. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  302. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  303. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  304. {gea_program-0.55.0 → gea_program-0.57.0}/gea/reference/volve_well_picks.csv.gz +0 -0
  305. {gea_program-0.55.0 → gea_program-0.57.0}/gea/report_history.py +0 -0
  306. {gea_program-0.55.0 → gea_program-0.57.0}/gea/rig_state.py +0 -0
  307. {gea_program-0.55.0 → gea_program-0.57.0}/gea/rock_inventory.py +0 -0
  308. {gea_program-0.55.0 → gea_program-0.57.0}/gea/sample_record.py +0 -0
  309. {gea_program-0.55.0 → gea_program-0.57.0}/gea/sample_well_profile.csv +0 -0
  310. {gea_program-0.55.0 → gea_program-0.57.0}/gea/sbom.py +0 -0
  311. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seedlink.py +0 -0
  312. {gea_program-0.55.0 → gea_program-0.57.0}/gea/segy.py +0 -0
  313. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic.py +0 -0
  314. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_array.py +0 -0
  315. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_assoc.py +0 -0
  316. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_detect.py +0 -0
  317. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_film.py +0 -0
  318. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_harmonic.py +0 -0
  319. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_qc.py +0 -0
  320. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_response.py +0 -0
  321. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_signature.py +0 -0
  322. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_track.py +0 -0
  323. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_uncertainty.py +0 -0
  324. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_unlisted.py +0 -0
  325. {gea_program-0.55.0 → gea_program-0.57.0}/gea/seismic_watch.py +0 -0
  326. {gea_program-0.55.0 → gea_program-0.57.0}/gea/sensor_swap.py +0 -0
  327. {gea_program-0.55.0 → gea_program-0.57.0}/gea/series_cache.py +0 -0
  328. {gea_program-0.55.0 → gea_program-0.57.0}/gea/service_life.py +0 -0
  329. {gea_program-0.55.0 → gea_program-0.57.0}/gea/setup_checklist.py +0 -0
  330. {gea_program-0.55.0 → gea_program-0.57.0}/gea/shapefile.py +0 -0
  331. {gea_program-0.55.0 → gea_program-0.57.0}/gea/shell.py +0 -0
  332. {gea_program-0.55.0 → gea_program-0.57.0}/gea/shut_in.py +0 -0
  333. {gea_program-0.55.0 → gea_program-0.57.0}/gea/simulation.py +0 -0
  334. {gea_program-0.55.0 → gea_program-0.57.0}/gea/site_map.py +0 -0
  335. {gea_program-0.55.0 → gea_program-0.57.0}/gea/sla_report.py +0 -0
  336. {gea_program-0.55.0 → gea_program-0.57.0}/gea/slip.py +0 -0
  337. {gea_program-0.55.0 → gea_program-0.57.0}/gea/soak.py +0 -0
  338. {gea_program-0.55.0 → gea_program-0.57.0}/gea/sra.py +0 -0
  339. {gea_program-0.55.0 → gea_program-0.57.0}/gea/store_forward.py +0 -0
  340. {gea_program-0.55.0 → gea_program-0.57.0}/gea/strata_join.py +0 -0
  341. {gea_program-0.55.0 → gea_program-0.57.0}/gea/structure.py +0 -0
  342. {gea_program-0.55.0 → gea_program-0.57.0}/gea/supervisor.py +0 -0
  343. {gea_program-0.55.0 → gea_program-0.57.0}/gea/survey_cmd.py +0 -0
  344. {gea_program-0.55.0 → gea_program-0.57.0}/gea/survey_view.py +0 -0
  345. {gea_program-0.55.0 → gea_program-0.57.0}/gea/telemetry.py +0 -0
  346. {gea_program-0.55.0 → gea_program-0.57.0}/gea/tls.py +0 -0
  347. {gea_program-0.55.0 → gea_program-0.57.0}/gea/tool_catalog.py +0 -0
  348. {gea_program-0.55.0 → gea_program-0.57.0}/gea/tool_library.py +0 -0
  349. {gea_program-0.55.0 → gea_program-0.57.0}/gea/tool_taxonomy.py +0 -0
  350. {gea_program-0.55.0 → gea_program-0.57.0}/gea/torque_drag.py +0 -0
  351. {gea_program-0.55.0 → gea_program-0.57.0}/gea/transient.py +0 -0
  352. {gea_program-0.55.0 → gea_program-0.57.0}/gea/vibration.py +0 -0
  353. {gea_program-0.55.0 → gea_program-0.57.0}/gea/well3d.py +0 -0
  354. {gea_program-0.55.0 → gea_program-0.57.0}/gea/well_assembler.py +0 -0
  355. {gea_program-0.55.0 → gea_program-0.57.0}/gea/well_report.py +0 -0
  356. {gea_program-0.55.0 → gea_program-0.57.0}/gea/well_test_validation.py +0 -0
  357. {gea_program-0.55.0 → gea_program-0.57.0}/gea/wellplan.py +0 -0
  358. {gea_program-0.55.0 → gea_program-0.57.0}/gea/wellpos.py +0 -0
  359. {gea_program-0.55.0 → gea_program-0.57.0}/gea/wits0.py +0 -0
  360. {gea_program-0.55.0 → gea_program-0.57.0}/gea/witsml.py +0 -0
  361. {gea_program-0.55.0 → gea_program-0.57.0}/gea/workspace.py +0 -0
  362. {gea_program-0.55.0 → gea_program-0.57.0}/gea/xlsx_read.py +0 -0
  363. {gea_program-0.55.0 → gea_program-0.57.0}/gea_program.egg-info/dependency_links.txt +0 -0
  364. {gea_program-0.55.0 → gea_program-0.57.0}/gea_program.egg-info/entry_points.txt +0 -0
  365. {gea_program-0.55.0 → gea_program-0.57.0}/gea_program.egg-info/requires.txt +0 -0
  366. {gea_program-0.55.0 → gea_program-0.57.0}/gea_program.egg-info/top_level.txt +0 -0
  367. {gea_program-0.55.0 → gea_program-0.57.0}/setup.cfg +0 -0
  368. {gea_program-0.55.0 → gea_program-0.57.0}/tests/test_acceptance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gea-program
3
- Version: 0.55.0
3
+ Version: 0.57.0
4
4
  Summary: GEA-Program: downhole gauge monitoring - canonical measurement records, quality rules, drift evaluation with a re-fit change log, well-test validation, alarm management, model cards, store-and-forward, monthly SLA measurement, FAT/SAT protocols and a dashboard, generated from live objects in the client's reporting outline.
5
5
  Author: Daniel T. Murphy
6
6
  License: MPL-2.0
@@ -1272,6 +1272,38 @@ Or Administration -> **HTTPS** in the page (download the certificate, not
1272
1272
  the key, for the site's computers to trust; compare the SHA-256 shown).
1273
1273
  `gea help https`.
1274
1274
 
1275
+ ### Company sign-in
1276
+
1277
+ People sign in with the account the company already manages - Microsoft
1278
+ Entra ID, Okta, Google Workspace, Ping, Keycloak, AD FS - by OpenID Connect
1279
+ (the authorization code flow with PKCE), so the company's multi-factor,
1280
+ conditional access and leavers apply. The ID token is checked with the
1281
+ standard library alone (its RS256 signature against the provider's
1282
+ published keys, the issuer, the audience, the times, the nonce); the role in
1283
+ GEA is the highest any of the person's app roles or groups maps to. Local
1284
+ accounts stay, for the first administrator and for when the provider cannot
1285
+ be reached. HTTPS comes first (above).
1286
+
1287
+ ```
1288
+ gea oidc --workspace C:\site --issuer https://login.microsoftonline.com/3f8b2c1d-6a4e-4d9b-8c2f-1e7a5b9d0c64/v2.0 --client-id a41c7e92-0d3b-4f6a-9e85-2b6d1c4f7a30 --redirect-uri https://gea-pad3.example.local:8765/api/oidc/callback --secret-from-env GEA_OIDC_SECRET --map GEA.Operator=operator --map GEA.Admin=admin --test
1289
+ ```
1290
+
1291
+ Or Administration -> **Company sign-in**. `gea help company-sign-in` has the
1292
+ Entra ID app registration step by step.
1293
+
1294
+ ### For an IT department
1295
+
1296
+ `docs/IT_SECURITY.md` (`gea security` prints it from any installation)
1297
+ answers what a reviewer asks before installing: where the data is and every
1298
+ connection the program can make, the one port, HTTPS, company sign-in and
1299
+ roles, the audit log, the bill of materials (`gea sbom`), and how a kit is
1300
+ checked - the build's attestation
1301
+ (`gea-program-0.57.0-win64.zip` checked with
1302
+ `gh attestation verify gea-program-0.57.0-win64.zip -R Daniel8Murphy0007/GEA-Program`)
1303
+ and the file catalog beside the kit's folder, Authenticode-signed once the
1304
+ publisher's certificate is in place. `SECURITY.md` says how to report a
1305
+ vulnerability.
1306
+
1275
1307
  ### Before a site goes live
1276
1308
 
1277
1309
  `deploy/README.md` is the checklist: HTTPS from the service itself (above),
@@ -1403,4 +1435,5 @@ https://pypi.org/project/gea-program/ after the first successful run.
1403
1435
  ## Licence
1404
1436
 
1405
1437
  Mozilla Public License 2.0 (MPL-2.0); see `LICENSE`. Every source file carries the MPL-2.0
1406
- header (Exhibit A). Copyright (c) 2026 Daniel T. Murphy.
1438
+ header (Exhibit A). Copyright (c) 2026 Daniel T. Murphy. The signed kit, its updates and
1439
+ support are a subscription from ENRGYONE (`docs/commercial/COMMERCIAL.md`).
@@ -1233,6 +1233,38 @@ Or Administration -> **HTTPS** in the page (download the certificate, not
1233
1233
  the key, for the site's computers to trust; compare the SHA-256 shown).
1234
1234
  `gea help https`.
1235
1235
 
1236
+ ### Company sign-in
1237
+
1238
+ People sign in with the account the company already manages - Microsoft
1239
+ Entra ID, Okta, Google Workspace, Ping, Keycloak, AD FS - by OpenID Connect
1240
+ (the authorization code flow with PKCE), so the company's multi-factor,
1241
+ conditional access and leavers apply. The ID token is checked with the
1242
+ standard library alone (its RS256 signature against the provider's
1243
+ published keys, the issuer, the audience, the times, the nonce); the role in
1244
+ GEA is the highest any of the person's app roles or groups maps to. Local
1245
+ accounts stay, for the first administrator and for when the provider cannot
1246
+ be reached. HTTPS comes first (above).
1247
+
1248
+ ```
1249
+ gea oidc --workspace C:\site --issuer https://login.microsoftonline.com/3f8b2c1d-6a4e-4d9b-8c2f-1e7a5b9d0c64/v2.0 --client-id a41c7e92-0d3b-4f6a-9e85-2b6d1c4f7a30 --redirect-uri https://gea-pad3.example.local:8765/api/oidc/callback --secret-from-env GEA_OIDC_SECRET --map GEA.Operator=operator --map GEA.Admin=admin --test
1250
+ ```
1251
+
1252
+ Or Administration -> **Company sign-in**. `gea help company-sign-in` has the
1253
+ Entra ID app registration step by step.
1254
+
1255
+ ### For an IT department
1256
+
1257
+ `docs/IT_SECURITY.md` (`gea security` prints it from any installation)
1258
+ answers what a reviewer asks before installing: where the data is and every
1259
+ connection the program can make, the one port, HTTPS, company sign-in and
1260
+ roles, the audit log, the bill of materials (`gea sbom`), and how a kit is
1261
+ checked - the build's attestation
1262
+ (`gea-program-0.57.0-win64.zip` checked with
1263
+ `gh attestation verify gea-program-0.57.0-win64.zip -R Daniel8Murphy0007/GEA-Program`)
1264
+ and the file catalog beside the kit's folder, Authenticode-signed once the
1265
+ publisher's certificate is in place. `SECURITY.md` says how to report a
1266
+ vulnerability.
1267
+
1236
1268
  ### Before a site goes live
1237
1269
 
1238
1270
  `deploy/README.md` is the checklist: HTTPS from the service itself (above),
@@ -1364,4 +1396,5 @@ https://pypi.org/project/gea-program/ after the first successful run.
1364
1396
  ## Licence
1365
1397
 
1366
1398
  Mozilla Public License 2.0 (MPL-2.0); see `LICENSE`. Every source file carries the MPL-2.0
1367
- header (Exhibit A). Copyright (c) 2026 Daniel T. Murphy.
1399
+ header (Exhibit A). Copyright (c) 2026 Daniel T. Murphy. The signed kit, its updates and
1400
+ support are a subscription from ENRGYONE (`docs/commercial/COMMERCIAL.md`).
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.55.0"
126
+ __version__ = "0.57.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -67,7 +67,16 @@ def _build_config(a):
67
67
  return SimulatorConfig(**kw)
68
68
 
69
69
 
70
+ # the front door's own commands (gea.cli), reached the same way through `python -m gea` - the kit's gea.cmd and
71
+ # gea wrapper run `python -m gea`, and before v0.57 `gea guide`, `gea help` and the rest answered "invalid choice"
72
+ FRONT_ONLY = ('quickstart', 'guide', 'security', 'docs', 'gui', 'help')
73
+
74
+
70
75
  def main(argv=None) -> int:
76
+ _argv = list(sys.argv[1:] if argv is None else argv)
77
+ if _argv and _argv[0] in FRONT_ONLY:
78
+ from .cli import main as _front
79
+ return _front(_argv)
71
80
  ap = argparse.ArgumentParser(prog="gea",
72
81
  description="GEA Downhole Simulator - headless CLI")
73
82
  sub = ap.add_subparsers(dest="cmd", required=True)
@@ -776,6 +785,24 @@ def main(argv=None) -> int:
776
785
  p_tl.add_argument("--actor", type=str, default="cli")
777
786
  p_tl.add_argument("--json", action="store_true")
778
787
 
788
+ p_oc = sub.add_parser("oidc", help="company sign-in (OpenID Connect: Entra ID, Okta, Google, Ping, Keycloak, AD FS): set it, test the provider, remove it; with no option, how it stands")
789
+ p_oc.add_argument("--workspace", type=str, required=True)
790
+ p_oc.add_argument("--issuer", type=str, default=None, help="Entra ID: https://login.microsoftonline.com/3f8b2c1d-6a4e-4d9b-8c2f-1e7a5b9d0c64/v2.0")
791
+ p_oc.add_argument("--client-id", type=str, default=None)
792
+ p_oc.add_argument("--redirect-uri", type=str, default=None, help="https://gea-pad3.example.local:8765/api/oidc/callback, registered with the provider exactly")
793
+ p_oc.add_argument("--secret-from-env", type=str, default=None, help="read the client secret from this environment variable now and keep it in keys\\oidc_client_secret.txt")
794
+ p_oc.add_argument("--secret-env", type=str, default=None, help="or: the service reads the client secret from this environment variable at each sign-in (nothing kept)")
795
+ p_oc.add_argument("--role-claim", type=str, default=None, help="roles (Entra ID app roles, the default) or groups, or any claim")
796
+ p_oc.add_argument("--map", action="append", default=[], help="VALUE=ROLE, e.g. GEA.Admin=admin (repeat)")
797
+ p_oc.add_argument("--default-role", type=str, default=None, help="the role of a person with no mapped value; 'none' (the default) means no sign-in")
798
+ p_oc.add_argument("--name-claim", type=str, default=None, help="the claim that names the person (preferred_username by default)")
799
+ p_oc.add_argument("--label", type=str, default=None, help="the sign-in button's words")
800
+ p_oc.add_argument("--pause", action="store_true", help="keep the settings, stop company sign-in")
801
+ p_oc.add_argument("--test", action="store_true", help="read the provider's discovery document and keys now")
802
+ p_oc.add_argument("--off", action="store_true", help="remove company sign-in (local accounts are unchanged)")
803
+ p_oc.add_argument("--actor", type=str, default="cli")
804
+ p_oc.add_argument("--json", action="store_true")
805
+
779
806
  p_hk = sub.add_parser("housekeeping", help="rotate the append-only logs, prune finished job folders and old live recordings (dry run unless --apply)")
780
807
  p_hk.add_argument("--workspace", type=str, required=True)
781
808
  p_hk.add_argument("--apply", action="store_true")
@@ -2616,6 +2643,53 @@ def main(argv=None) -> int:
2616
2643
  raise SystemExit(f"pore-pressure: {e.args[0] if e.args else e}")
2617
2644
  print(_json.dumps(r, default=str) if a.json else _PPF.text(r))
2618
2645
  return 0
2646
+ elif a.cmd == "oidc":
2647
+ import json as _json
2648
+ from . import oidc as _OI
2649
+ from .workspace import Workspace, WorkspaceError
2650
+ try:
2651
+ ws = Workspace(a.workspace)
2652
+ changing = any(x is not None for x in (a.issuer, a.client_id, a.redirect_uri, a.secret_from_env, a.secret_env, a.role_claim,
2653
+ a.default_role, a.name_claim, a.label)) or a.map or a.pause
2654
+ if a.off:
2655
+ r = _OI.turn_off(ws, a.actor)
2656
+ elif changing:
2657
+ cur = _OI.settings(ws) or {}
2658
+ s = {k: cur.get(k, v) for k, v in _OI.DEFAULTS.items()}
2659
+ for k, v in (("issuer", a.issuer), ("client_id", a.client_id), ("redirect_uri", a.redirect_uri), ("role_claim", a.role_claim),
2660
+ ("name_claim", a.name_claim), ("label", a.label), ("client_secret_env", a.secret_env)):
2661
+ if v is not None:
2662
+ s[k] = v
2663
+ if a.default_role is not None:
2664
+ s["default_role"] = None if a.default_role.lower() in ("", "none") else a.default_role
2665
+ if a.map:
2666
+ m = {}
2667
+ for x in a.map:
2668
+ v_, _, r_ = x.rpartition("=")
2669
+ if not v_:
2670
+ raise ValueError(f"--map takes VALUE=ROLE, not {x!r}")
2671
+ m[v_.strip()] = r_.strip()
2672
+ s["role_map"] = m
2673
+ if a.secret_from_env:
2674
+ s["client_secret_env"] = ""
2675
+ s["enabled"] = not a.pause
2676
+ sec = None
2677
+ if a.secret_from_env:
2678
+ sec = os.environ.get(a.secret_from_env, "")
2679
+ if not sec:
2680
+ raise ValueError(f"the environment variable {a.secret_from_env} is empty")
2681
+ r = _OI.configure(ws, a.actor, s, sec)
2682
+ else:
2683
+ r = _OI.status(ws)
2684
+ if a.test:
2685
+ r = {**r, "test": _OI.test(ws)}
2686
+ except (KeyError, ValueError, OSError, WorkspaceError) as e:
2687
+ raise SystemExit(f"oidc: {e.args[0] if e.args else e}")
2688
+ if a.json:
2689
+ print(_json.dumps(r, default=str))
2690
+ else:
2691
+ print(_OI.text(r))
2692
+ return 0
2619
2693
  elif a.cmd == "tls":
2620
2694
  import json as _json
2621
2695
  from . import tls as _TLS
@@ -12461,6 +12461,366 @@ def section_cx_https(tmp: str) -> None:
12461
12461
  "no dossier carries the key; the help page" + (f" - failed: {failed4}" if failed4 else ""))
12462
12462
 
12463
12463
 
12464
+ def section_cy_company_sign_in(tmp: str) -> None:
12465
+ """Section CY - company sign-in (OpenID Connect): against a stand-in provider over HTTPS, the whole browser round
12466
+ trip - the start, the provider, the code exchanged with PKCE and the secret, the ID token checked, the role from the
12467
+ claims, a session - and every way an ID token or a return can be wrong turned away; the company account beside the
12468
+ local ones; the settings, the secret kept out of sight; the CLI, the page and the help."""
12469
+ import http.client
12470
+ import ssl as _ssl
12471
+ import subprocess as _sp
12472
+ import urllib.parse as _up
12473
+ import zipfile as _zf
12474
+ from . import __version__ as _ver
12475
+ from . import dossier as DZ
12476
+ from . import helplib as H
12477
+ from . import oidc as OI
12478
+ from . import tls as TLS
12479
+ from .oidc_mock import MockIdP
12480
+ from .service import Service, Users
12481
+ from .workspace import Workspace
12482
+ pkg = Path(__file__).parent
12483
+ idp = MockIdP()
12484
+ wsp = str(Path(tmp, "ws_cy")); ws = Workspace.create(wsp, "Company Site", actor="tester")
12485
+ users = Users(str(Path(wsp, "users.json")))
12486
+ users.add("adm", "admin-pass-123", "admin"); users.add("v", "viewer-pass-1", "viewer"); users.add("olga.local", "local-pass-12", "operator")
12487
+ TLS.make_self_signed(ws, "tester", ["localhost", "127.0.0.1"], days=60)
12488
+ svc = Service(wsp, host="127.0.0.1", port=0, scheduler=False)
12489
+ svc.app.oidc_ssl_context = idp.client_ctx
12490
+ svc.start()
12491
+ base = svc.url.rstrip("/")
12492
+ trust = _ssl.create_default_context(cafile=str(Path(wsp, TLS.SELF_CERT)))
12493
+
12494
+ def get(url, cookie=None, ctx=trust):
12495
+ u = _up.urlsplit(url)
12496
+ c = http.client.HTTPSConnection(u.hostname, u.port, context=ctx, timeout=30)
12497
+ c.request("GET", u.path + ("?" + u.query if u.query else ""), headers=({"Cookie": cookie} if cookie else {}))
12498
+ r = c.getresponse()
12499
+ b = r.read()
12500
+ c.close()
12501
+ return r.status, r.getheader("Location"), r.headers.get_all("Set-Cookie") or [], b
12502
+
12503
+ def api(method, path, body=None, cookie=None):
12504
+ c = http.client.HTTPSConnection("127.0.0.1", svc.port, context=trust, timeout=30)
12505
+ h = {"Content-Type": "application/json", "X-GEA-Action": "1"} if method == "POST" else {}
12506
+ if cookie:
12507
+ h["Cookie"] = cookie
12508
+ c.request(method, path, body=(json.dumps(body) if body is not None else None), headers=h)
12509
+ r = c.getresponse()
12510
+ b = r.read()
12511
+ c.close()
12512
+ return r.status, json.loads(b or b"{}"), r.headers.get_all("Set-Cookie") or []
12513
+
12514
+ def local(name, pw):
12515
+ return [x for x in api("POST", "/api/login", {"name": name, "password": pw})[2] if x.startswith("gea_session")][0].split(";")[0]
12516
+
12517
+ def round_trip(user=None, fault=None, keep_cookie=True):
12518
+ """The browser's path: start -> the provider -> back. (status, the page, the session cookie or None, the callback URL, the flow cookie)"""
12519
+ idp.fault = fault
12520
+ if user:
12521
+ idp.user = user
12522
+ s1, loc1, ck1, _ = get(base + "/api/oidc/start")
12523
+ flow = ck1[0].split(";")[0] if ck1 else None
12524
+ s2, loc2, _, _ = get(loc1, ctx=idp.client_ctx)
12525
+ s3, _, ck3, b3 = get(loc2, cookie=(flow if keep_cookie else None))
12526
+ sess = next((x.split(";")[0] for x in ck3 if x.startswith("gea_session=") and not x.startswith("gea_session=;")), None)
12527
+ return s3, b3.decode("utf-8", "replace"), sess, loc2, flow, (s1, s2, ck1)
12528
+ olga = {"sub": "sub-olga", "preferred_username": "olga@example.com", "roles": ["GEA.Operator"]}
12529
+ try:
12530
+ adm = local("adm", "admin-pass-123"); vw = local("v", "viewer-pass-1")
12531
+ # -- CY1 the settings --------------------------------------------------------------------------------------------
12532
+ redir = base + "/api/oidc/callback"
12533
+ good = {"issuer": idp.issuer, "client_id": idp.client_id, "redirect_uri": redir,
12534
+ "role_map": {"GEA.Viewer": "viewer", "GEA.Operator": "operator", "GEA.Approver": "approver", "GEA.Admin": "admin"}}
12535
+ bad = [{**good, "redirect_uri": "http://gea-pad3.example.local:8765/api/oidc/callback"}, {**good, "redirect_uri": base + "/somewhere"},
12536
+ {**good, "issuer": "https://login.microsoftonline.com/common/v2.0"}, {**good, "issuer": "http://idp.example"},
12537
+ {**good, "role_map": {}}, {**good, "role_map": {"x": "superuser"}}, {**good, "client_id": ""}]
12538
+ bad_codes = [api("POST", "/api/oidc", {"action": "set", **b, "client_secret": "s"}, adm)[0] for b in bad]
12539
+ s_vset = api("POST", "/api/oidc", {"action": "set", **good, "client_secret": idp.client_secret}, vw)[0]
12540
+ s_set, st_set, _ = api("POST", "/api/oidc", {"action": "set", **good, "client_secret": idp.client_secret}, adm)
12541
+ s_get, st_get, _ = api("GET", "/api/oidc", cookie=adm)
12542
+ s_vget = api("GET", "/api/oidc", cookie=vw)[0]
12543
+ s_test, tst, _ = api("POST", "/api/oidc", {"action": "test"}, adm)
12544
+ sess_pub = api("GET", "/api/session")[1]
12545
+ secret_file = Path(wsp, OI.SECRET_FILE)
12546
+ c1 = {
12547
+ "refused": all(c == 400 for c in bad_codes) and s_vset == 403 and s_vget == 403,
12548
+ "kept": s_set == 200 and st_set["configured"] and st_set["secret"] == "kept" and s_get == 200 and st_get["issuer"] == idp.issuer
12549
+ and idp.client_secret not in json.dumps(st_get) and secret_file.read_text(encoding="utf-8") == idp.client_secret
12550
+ and (os.name == "nt" or (secret_file.stat().st_mode & 0o077) == 0),
12551
+ "test": s_test == 200 and tst["ok"] and tst["signing_keys"] == 1 and tst["token_endpoint"].endswith("/token"),
12552
+ "public": sess_pub.get("company_sign_in") == {"label": "Sign in with your company account"} and sess_pub.get("user") is None,
12553
+ }
12554
+ failed1 = [k for k, v in c1.items() if not v]
12555
+ ok(not failed1,
12556
+ "CY1 company sign-in is set by an administrator only: an http:// or wrong redirect URI, a multi-tenant issuer, an issuer not on https, no role map "
12557
+ "and no default, a role that does not exist, no client id - each refused; the secret is kept in keys\\ readable by its owner only and never sent "
12558
+ "back; Test reads the provider's discovery and keys; the sign-in page learns only that the button exists" + (f" - failed: {failed1}" if failed1 else ""))
12559
+ # -- CY2 the round trip --------------------------------------------------------------------------------------------
12560
+ s3, page, sess, cb_url, flow, (s1, s2, ck1) = round_trip(olga)
12561
+ me = api("GET", "/api/session", cookie=sess)[1]["user"] if sess else {}
12562
+ call = idp.token_calls[-1] if idp.token_calls else {}
12563
+ r_replay = get(cb_url, cookie=flow)
12564
+ s_replay, replay_body = r_replay[0], r_replay[3].decode()
12565
+ idp.user = dict(olga, roles=["GEA.Operator", "GEA.Admin"])
12566
+ s_up, _, sess_up, _, _, _ = round_trip()
12567
+ me_up = api("GET", "/api/session", cookie=sess_up)[1]["user"] if sess_up else {}
12568
+ listed = {u["name"]: u for u in api("GET", "/api/users", cookie=adm)[1]["users"]}
12569
+ live = [x for x in api("GET", "/api/sessions", cookie=adm)[1]["sessions"] if x["name"] == "olga@example.com"]
12570
+ c2 = {
12571
+ "start": s1 == 302 and "SameSite=Lax" in ck1[0] and "Secure" in ck1[0] and "Path=/api/oidc" in ck1[0] and s2 == 302,
12572
+ "back": s3 == 200 and 'http-equiv="refresh"' in page and sess is not None and me.get("name") == "olga@example.com" and me.get("role") == "operator"
12573
+ and me.get("via") == "company",
12574
+ "pkce": call.get("code_verifier") and call.get("client_secret") == idp.client_secret and call.get("redirect_uri") == redir,
12575
+ "once": s_replay == 401 and "unknown or has expired" in replay_body,
12576
+ "role_follows": me_up.get("role") == "admin" and listed["olga@example.com"]["kind"] == "company" and listed["olga@example.com"]["role"] == "admin"
12577
+ and live and all(x["via"] == "company" for x in live),
12578
+ }
12579
+ failed2 = [k for k, v in c2.items() if not v]
12580
+ ok(not failed2,
12581
+ "CY2 the round trip: the start sends the browser to the provider with a flow cookie (SameSite=Lax, Secure, this path only); back with a code, the "
12582
+ "service exchanges it with the client secret and the PKCE verifier, checks the ID token, and the person is signed in as a company account with the "
12583
+ "role their claims map to - the highest, taken again at every sign-in - by a page that moves on by itself (the session cookie stays Strict); "
12584
+ "a return is accepted once" + (f" - failed: {failed2}" if failed2 else ""))
12585
+ # -- CY3 every way it can be wrong ----------------------------------------------------------------------------------
12586
+ outcomes = {}
12587
+ for f in ("aud", "expired", "sig", "alg_none", "hs256", "nonce", "iss"):
12588
+ st_, pg_, se_, _, _, _ = round_trip(olga, fault=f)
12589
+ outcomes[f] = (st_, se_ is None, pg_)
12590
+ st_rot, _, se_rot, _, _, _ = round_trip(olga, fault="rotate")
12591
+ st_nb, pg_nb, se_nb, _, _, _ = round_trip(olga, keep_cookie=False)
12592
+ st_nr, pg_nr, se_nr, _, _, _ = round_trip({"sub": "sub-x", "preferred_username": "x@example.com", "roles": ["Other"]})
12593
+ st_lc, pg_lc, se_lc, _, _, _ = round_trip({"sub": "sub-l", "preferred_username": "olga.local", "roles": ["GEA.Admin"]})
12594
+ st_os, pg_os, se_os, _, _, _ = round_trip({"sub": "someone-else", "preferred_username": "olga@example.com", "roles": ["GEA.Admin"]})
12595
+ pg_ov = ""
12596
+ try:
12597
+ OI.role_of({"_claim_names": {"groups": "src1"}}, {**OI.DEFAULTS, "role_claim": "groups", "role_map": {"g": "viewer"}})
12598
+ except OI.OidcError as e:
12599
+ pg_ov = str(e)
12600
+ s_dis = api("POST", "/api/users/disable", {"name": "olga@example.com", "disabled": True}, adm)[0]
12601
+ st_dis, pg_dis, se_dis, _, _, _ = round_trip(olga)
12602
+ api("POST", "/api/users/disable", {"name": "olga@example.com", "disabled": False}, adm)
12603
+ s_pw = api("POST", "/api/users/password", {"name": "olga@example.com", "password": "whatever-123"}, adm)[0]
12604
+ s_role = api("POST", "/api/users/role", {"name": "olga@example.com", "role": "viewer"}, adm)[0]
12605
+ s_pwlogin = api("POST", "/api/login", {"name": "olga@example.com", "password": ""})[0]
12606
+ s_local = api("POST", "/api/login", {"name": "olga.local", "password": "local-pass-12"})[0]
12607
+ audit = Path(ws.audit_path).read_text(encoding="utf-8")
12608
+ why = {"aud": "audience", "expired": "expired", "sig": "signature", "alg_none": "only RS256", "hs256": "only RS256", "nonce": "nonce", "iss": "issued by"}
12609
+ c3 = {
12610
+ "tokens": all(outcomes[f][0] == 401 and outcomes[f][1] and why[f] in outcomes[f][2] for f in why),
12611
+ "rotation": st_rot == 200 and se_rot is not None,
12612
+ "binding": st_nb == 401 and se_nb is None and "another browser" in pg_nb,
12613
+ "no_role": st_nr == 401 and se_nr is None and "no role in GEA" in pg_nr and "Other" in pg_nr,
12614
+ "no_takeover": st_lc == 401 and se_lc is None and "local account" in pg_lc and st_os == 401 and se_os is None and "another company identity" in pg_os,
12615
+ "overage": "more groups than a token carries" in pg_ov,
12616
+ "disabled": s_dis == 200 and st_dis == 401 and se_dis is None and "disabled" in pg_dis,
12617
+ "no_password": s_pw == 400 and s_role == 400 and s_pwlogin in (400, 401) and s_local == 200,
12618
+ "audited": audit.count('"via": "company"') >= 3 and '"login.failed"' in audit and "audience" in audit,
12619
+ }
12620
+ failed3 = [k for k, v in c3.items() if not v]
12621
+ ok(not failed3,
12622
+ "CY3 turned away, each saying why, with no session: an ID token for another application, expired, with a bad signature, signed 'none' or HS256, "
12623
+ "with another sign-in's nonce, from another issuer; a return to a browser that did not start the sign-in; a person with no mapped role; a name "
12624
+ "a local account has, or a company account bound to another subject; a person in more groups than a token carries; a company account disabled "
12625
+ "in GEA. A rotated signing key is fetched and accepted. A company account has no password here and its role is not set by hand; local accounts "
12626
+ "sign in as before; every sign-in and failure is audited" + (f" - failed: {failed3}" if failed3 else ""))
12627
+ finally:
12628
+ svc.stop()
12629
+ # -- CY4 the CLI, the dossier, the page, the help -------------------------------------------------------------------
12630
+ env = {**os.environ, "GEA_OIDC_TEST_SECRET": "cli-secret-456", "SSL_CERT_FILE": idp.cafile}
12631
+ w2 = str(Path(tmp, "ws_cy2")); Workspace.create(w2, "CLI Site", actor="tester")
12632
+ cli_set = _sp.run([sys.executable, "-m", "gea", "oidc", "--workspace", w2, "--issuer", idp.issuer, "--client-id", idp.client_id,
12633
+ "--redirect-uri", "https://gea-pad3.example.local:8765/api/oidc/callback", "--secret-from-env", "GEA_OIDC_TEST_SECRET",
12634
+ "--map", "GEA.Admin=admin", "--map", "GEA.Viewer=viewer", "--default-role", "none", "--test"],
12635
+ capture_output=True, text=True, cwd=str(pkg.parent), env=env)
12636
+ cli_st = _sp.run([sys.executable, "-m", "gea", "oidc", "--workspace", w2, "--json"], capture_output=True, text=True, cwd=str(pkg.parent), env=env)
12637
+ cli_bad = _sp.run([sys.executable, "-m", "gea", "oidc", "--workspace", w2, "--map", "nonsense"], capture_output=True, text=True, cwd=str(pkg.parent), env=env)
12638
+ cli_off = _sp.run([sys.executable, "-m", "gea", "oidc", "--workspace", w2, "--off"], capture_output=True, text=True, cwd=str(pkg.parent), env=env)
12639
+ idp.stop()
12640
+ __import__("gea.frac_hits", fromlist=["write_report"]).write_report(ws, _ver, "tester")
12641
+ dz = DZ.build(ws, "tester", _ver, None, include_sources=True)
12642
+ with _zf.ZipFile(Path(ws.reports_dir, dz["path"])) as z:
12643
+ leaked = [n for n in z.namelist() if idp.client_secret.encode() in z.read(n)]
12644
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
12645
+ st_cli = json.loads(cli_st.stdout) if cli_st.returncode == 0 else {}
12646
+ c4 = {
12647
+ "cli": cli_set.returncode == 0 and "provider: GOOD" in cli_set.stdout and "GEA.Admin -> admin" in cli_set.stdout
12648
+ and st_cli.get("secret") == "kept" and Path(w2, OI.SECRET_FILE).read_text(encoding="utf-8") == "cli-secret-456"
12649
+ and "cli-secret-456" not in cli_st.stdout and cli_bad.returncode != 0 and "VALUE=ROLE" in (cli_bad.stderr + cli_bad.stdout)
12650
+ and cli_off.returncode == 0 and "local accounts only" in cli_off.stdout,
12651
+ "dossier": not leaked and idp.client_secret not in Path(ws.audit_path).read_text(encoding="utf-8"),
12652
+ "page": all(x in page_src for x in ("/api/oidc/start", "aCompany", "s.company_sign_in", "ocSave", "ocTest", "ocMap", "company account: role from its claims"))
12653
+ and "[hidden] { display: none !important; }" in page_src,
12654
+ "help": not H.check("company-sign-in") and "Entra" in (H.page("company-sign-in") or "") and "App roles" in (H.page("company-sign-in") or ""),
12655
+ }
12656
+ failed4 = [k for k, v in c4.items() if not v]
12657
+ ok(not failed4,
12658
+ "CY4 `gea oidc` sets company sign-in from a terminal (the secret read from an environment variable once and kept, never printed), tests the "
12659
+ "provider, refuses a malformed map and removes it; no dossier carries the client secret; the sign-in page's button and the Company sign-in card; "
12660
+ "the help page with the Entra ID steps" + (f" - failed: {failed4}" if failed4 else ""))
12661
+
12662
+
12663
+ def section_cz_it_review(tmp: str) -> None:
12664
+ """Section CZ - what an IT department asks: the security overview ships with the program and agrees with the
12665
+ code it describes (the rounds, the sessions, the lock, the port, TLS, every host the program can reach); `gea
12666
+ security`, `gea guide` and `gea help` answer through `python -m gea` as the kit's launcher runs it; in a checkout,
12667
+ the kit tools zip the kit again with its catalog beside it and tell the site how to check it, and the build
12668
+ workflow attests both kits and signs the catalog only when a certificate is configured."""
12669
+ import importlib.util
12670
+ import json as _json
12671
+ import subprocess as _sp
12672
+ import urllib.parse as _up
12673
+ import zipfile as _zf
12674
+ from . import doctor as DR
12675
+ from . import elevation as EL
12676
+ from . import helplib as H
12677
+ from . import seismic_watch as SW
12678
+ from . import service as SVC
12679
+ from . import tls as TLS
12680
+ pkg = Path(__file__).parent
12681
+ checkout = (pkg.parent / "pyproject.toml").exists() and (pkg.parent / "tools" / "build_installer.py").exists()
12682
+
12683
+ # -- CZ1 the overview, and the code it describes -------------------------------------------------------------------
12684
+ text = H.security_text() or ""
12685
+ docs_copy = pkg.parent / "docs" / "IT_SECURITY.md"
12686
+ same = (not docs_copy.exists()) or docs_copy.read_text(encoding="utf-8") == text
12687
+ flat = " ".join(text.split())
12688
+ with open(Path(pkg, "reference", "eia_prices.provenance.json"), encoding="utf-8") as f:
12689
+ eia_hosts = {_up.urlsplit(s["refresh_url"]).hostname for s in _json.load(f)["series"].values() if s.get("refresh_url")}
12690
+ doctor_src = Path(DR.__file__).read_text(encoding="utf-8")
12691
+ hosts = {_up.urlsplit(EL.EPQS_URL).hostname, _up.urlsplit(SW.QUERY_URL).hostname, _up.urlsplit(SW.FEED_URL).hostname, *eia_hosts}
12692
+ hosts |= {"pypi.org"} if "https://pypi.org/pypi/gea-program/json" in doctor_src else {"(the update check moved)"}
12693
+ facts = {
12694
+ "rounds": f"{SVC.PBKDF2_ROUNDS:,}" in flat,
12695
+ "sessions": f"{SVC.SESSION_HOURS} hours" in flat and "256-bit" in flat,
12696
+ "lock": SVC.LOGIN_MAX_FAILURES == 5 and "five failures" in flat and f"{SVC.LOGIN_LOCK_S // 60} minutes" in flat,
12697
+ "port": "TCP 8765" in flat and "8765" in Path(DR.__file__).read_text(encoding="utf-8"),
12698
+ "tls": "TLS 1.2 minimum" in flat and f"last {TLS.EXPIRING_DAYS} days" in flat,
12699
+ "hosts": all(f"`{h}`" in flat for h in hosts),
12700
+ "no_telemetry": "No telemetry" in flat,
12701
+ }
12702
+ # Python 3.14's argparse formats every help string as the parser is built (help % params) and stops on a bare
12703
+ # "%": v0.54.0's first ship stopped at its gate that way while 3.13 passed. The same check, on any Python.
12704
+ import argparse as _ap
12705
+ import contextlib as _cl
12706
+ import io as _io
12707
+ from . import __main__ as _M
12708
+ bad_help = []
12709
+ _orig_add, _orig_sub = _ap.ArgumentParser.add_argument, _ap._SubParsersAction.add_parser
12710
+ _params = {k_: "x" for k_ in ("prog", "default", "dest", "type", "choices", "metavar", "required", "nargs", "const", "option_strings")}
12711
+
12712
+ def _fmt_ok(h):
12713
+ try:
12714
+ h % _params
12715
+ return True
12716
+ except (ValueError, TypeError, KeyError):
12717
+ return False
12718
+
12719
+ def _add(self, *a_, **k_):
12720
+ h_ = k_.get("help")
12721
+ if isinstance(h_, str) and h_ != _ap.SUPPRESS and not _fmt_ok(h_):
12722
+ bad_help.append(f"{self.prog} {'/'.join(a_)}")
12723
+ return _orig_add(self, *a_, **k_)
12724
+
12725
+ def _sub(self, name, **k_):
12726
+ h_ = k_.get("help")
12727
+ if isinstance(h_, str) and not _fmt_ok(h_):
12728
+ bad_help.append(f"gea {name}")
12729
+ return _orig_sub(self, name, **k_)
12730
+ _ap.ArgumentParser.add_argument, _ap._SubParsersAction.add_parser = _add, _sub
12731
+ try:
12732
+ with _cl.redirect_stdout(_io.StringIO()), _cl.redirect_stderr(_io.StringIO()):
12733
+ try:
12734
+ _M.main(["wells"])
12735
+ except SystemExit:
12736
+ pass
12737
+ finally:
12738
+ _ap.ArgumentParser.add_argument, _ap._SubParsersAction.add_parser = _orig_add, _orig_sub
12739
+ facts["help_strings"] = not bad_help
12740
+ r_sec = _sp.run([sys.executable, "-m", "gea", "security"], capture_output=True, text=True, encoding="utf-8", errors="replace")
12741
+ r_guide = _sp.run([sys.executable, "-m", "gea", "guide"], capture_output=True, text=True, encoding="utf-8", errors="replace")
12742
+ r_help = _sp.run([sys.executable, "-m", "gea", "help", "https"], capture_output=True, text=True, encoding="utf-8", errors="replace")
12743
+ bad = [k for k, v in facts.items() if not v]
12744
+ ok(text.startswith("# GEA-Program for an IT department") and same and not bad
12745
+ and r_sec.returncode == 0 and "GEA-Program for an IT department" in r_sec.stdout
12746
+ and r_guide.returncode == 0 and "HOW TO TEST GEA" in r_guide.stdout and r_help.returncode == 0 and "**The command**" in r_help.stdout,
12747
+ "CZ1 the security overview ships in the package (identical to docs/IT_SECURITY.md) and agrees with the code: the password hashing rounds, the "
12748
+ "session length and token size, the sign-in lock, the one port, the TLS floor and the expiry warning, every host the program can reach "
12749
+ "(ComCat, the elevation service, the price series, PyPI); `python -m gea security`, `guide` and `help` answer as the kit's own launcher "
12750
+ "runs them; every option's help text formats the way Python 3.14 checks it as the parser is built"
12751
+ + (f" - disagrees: {bad}" if bad else "") + (f" - help texts: {bad_help}" if bad_help else ""))
12752
+
12753
+ # -- CZ2 the kit is zipped again with its catalog, and says how to check it ----------------------------------------------
12754
+ if checkout:
12755
+ spec = importlib.util.spec_from_file_location("cz_installer", str(pkg.parent / "tools" / "build_installer.py"))
12756
+ bi = importlib.util.module_from_spec(spec); spec.loader.exec_module(bi)
12757
+ kit = Path(tmp, "cz_kit", "gea-program-9.9.9-win64"); (kit / "python").mkdir(parents=True)
12758
+ (kit / "MANIFEST.json").write_text("{}", encoding="utf-8"); (kit / "python" / "python.exe").write_bytes(b"MZ stand-in")
12759
+ (kit / "SHA256SUMS.txt").write_text("x MANIFEST.json\n", encoding="utf-8")
12760
+ cat = Path(tmp, "cz_kit", "gea-program-9.9.9-win64.cat"); cat.write_bytes(b"0\x82 a catalog stand-in")
12761
+ zp = bi.rezip(kit, [cat])
12762
+ with _zf.ZipFile(zp) as z:
12763
+ names = sorted(z.namelist())
12764
+ cat_back = z.read("gea-program-9.9.9-win64.cat")
12765
+ try:
12766
+ bi.rezip(Path(tmp, "cz_kit"), [])
12767
+ not_kit = "accepted"
12768
+ except SystemExit as e:
12769
+ not_kit = str(e)
12770
+ readme = bi.README.format(version="9.9.9", target="Windows x64", pyline="p", install="i", start="s", verify="v", port="8765",
12771
+ service="sv", stop="st", uninstall="u", workspace="w", check=bi.WIN_CHECK.format(version="9.9.9"))
12772
+ build_src = (pkg.parent / "tools" / "build_installer.py").read_text(encoding="utf-8")
12773
+ c2 = {
12774
+ "rezip": names == ["gea-program-9.9.9-win64.cat", "gea-program-9.9.9-win64/MANIFEST.json", "gea-program-9.9.9-win64/SHA256SUMS.txt",
12775
+ "gea-program-9.9.9-win64/python/python.exe"] and cat_back == cat.read_bytes() and "not a kit folder" in not_kit,
12776
+ "readme": all(x in readme for x in ("gh attestation verify gea-program-9.9.9-win64.zip -R Daniel8Murphy0007/GEA-Program",
12777
+ "Test-FileCatalog -CatalogFilePath .\\gea-program-9.9.9-win64.cat -Path .\\gea-program-9.9.9-win64",
12778
+ "Get-AuthenticodeSignature", "IT_SECURITY.md", "gea help https"))
12779
+ and "368 checks" not in readme and "behind the site's TLS proxy" not in readme,
12780
+ "ships_overview": "docs' / 'IT_SECURITY.md'" in build_src and "kit / 'IT_SECURITY.md'" in build_src,
12781
+ }
12782
+ where = "the checkout's kit tools"
12783
+ else:
12784
+ c2 = {"installed": bool(H.security_text())}
12785
+ where = "an installed copy: the kit tools are checked in the checkout"
12786
+ failed2 = [k for k, v in c2.items() if not v]
12787
+ ok(not failed2,
12788
+ f"CZ2 ({where}) the kit is zipped again from its folder with its file catalog beside the folder, every file kept; a folder that is not a kit "
12789
+ "is refused; the kit's README tells the site how to check the build's attestation and every file against the catalog and its signature, "
12790
+ "and carries the IT overview" + (f" - failed: {failed2}" if failed2 else ""))
12791
+
12792
+ # -- CZ3 the build workflow: attested, signed only with a certificate, never signed in name only ----------------------------
12793
+ wf = pkg.parent / ".github" / "workflows" / "build-installer.yml"
12794
+ if wf.exists():
12795
+ y = wf.read_text(encoding="utf-8")
12796
+ win = y[y.index("windows-kit:"):y.index("linux-kit:")]
12797
+ lin = y[y.index("linux-kit:"):]
12798
+ order = [win.index(s) for s in ("New-FileCatalog", "azure/artifact-signing-action", "signtool", "Test-FileCatalog", "--rezip", "call install.cmd",
12799
+ "attest-build-provenance", "attach-kit.sh")]
12800
+ c3 = {
12801
+ "permissions": "id-token: write" in y and "attestations: write" in y and "contents: write" in y,
12802
+ "attested": "actions/attest-build-provenance" in win and "actions/attest-build-provenance" in lin
12803
+ and "subject-path: dist/gea-program-*-win64.zip" in win and "subject-path: dist/gea-program-*-linux-*.zip" in lin,
12804
+ "conditional": "if: env.ARTIFACT_SIGNING_ACCOUNT != ''" in win and "if: env.ARTIFACT_SIGNING_ACCOUNT == '' && env.SIGNING_PFX_BASE64 != ''" in win
12805
+ and "secrets.ARTIFACT_SIGNING_ACCOUNT" in win and "secrets.SIGNING_PFX_BASE64" in win,
12806
+ "checked": "-CatalogVersion 2.0" in win and "throw 'the catalog does not match the kit'" in win
12807
+ and "throw 'a certificate is configured but the catalog is not validly signed'" in win and "ships unsigned" in win
12808
+ and "Remove-Item $pfx" in win and "/tr http://timestamp" in win and "timestamp-rfc3161" in win,
12809
+ "order": order == sorted(order),
12810
+ "policy": (pkg.parent / "SECURITY.md").exists() and "Report a vulnerability" in " ".join((pkg.parent / "SECURITY.md").read_text(encoding="utf-8").split()),
12811
+ }
12812
+ where3 = "the checkout's workflow"
12813
+ else:
12814
+ c3 = {"installed": True}
12815
+ where3 = "an installed copy: the workflow is checked in the checkout"
12816
+ failed3 = [k for k, v in c3.items() if not v]
12817
+ ok(not failed3,
12818
+ f"CZ3 ({where3}) both kits are attested by the build that made them; the Windows kit's file catalog is made, signed by Azure Artifact "
12819
+ "Signing or a PFX only when its secrets are configured (timestamped; the PFX removed), checked against the kit - a mismatch, or a "
12820
+ "configured certificate that did not sign, stops the build - then zipped beside the kit before the kit is installed and its gate run; "
12821
+ "SECURITY.md says how to report a vulnerability" + (f" - failed: {failed3}" if failed3 else ""))
12822
+
12823
+
12464
12824
  def PR_read_ok(path) -> bool:
12465
12825
  from . import pdf_report as PR
12466
12826
  try:
@@ -12574,6 +12934,8 @@ def main() -> int:
12574
12934
  section_cv_torque_drag(tmp)
12575
12935
  section_cw_decline(tmp)
12576
12936
  section_cx_https(tmp)
12937
+ section_cy_company_sign_in(tmp)
12938
+ section_cz_it_review(tmp)
12577
12939
  if _FAILS:
12578
12940
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
12579
12941
  for f in _FAILS:
@@ -10,6 +10,7 @@
10
10
  gea model-cards ... gea store-forward ... gea sla-report ... gea fat-sat ...
11
11
  gea help [topic] the help library, by the job (gea help drift); same text as the dashboard's panels
12
12
  gea guide the click-by-click tester guide
13
+ gea security what an IT department asks before installing, answered (docs/IT_SECURITY.md)
13
14
  gea docs where the catalogue and the reports live on this machine
14
15
  gea gui the desktop window (desktop extra)
15
16
  gea accept the product gate
@@ -23,7 +24,7 @@ import os
23
24
  import sys
24
25
  from pathlib import Path
25
26
 
26
- FRONT = {'quickstart', 'survey', 'guide', 'docs', 'gui', 'help', '-h', '--help'}
27
+ FRONT = {'quickstart', 'survey', 'guide', 'security', 'docs', 'gui', 'help', '-h', '--help'}
27
28
 
28
29
 
29
30
  def _pkg_root() -> Path:
@@ -62,6 +63,16 @@ def cmd_guide(_argv) -> int:
62
63
  return 0
63
64
 
64
65
 
66
+ def cmd_security(_argv) -> int:
67
+ from .helplib import security_text
68
+ text = security_text() # ships inside the package (gea/help/IT_SECURITY.md)
69
+ if text is None:
70
+ print('the security overview is missing from this installation; reinstall: python -m pip install --upgrade --force-reinstall "gea-program[live]"')
71
+ return 2
72
+ print(text)
73
+ return 0
74
+
75
+
65
76
  def cmd_help(argv) -> int:
66
77
  from .helplib import page, index_text, topics
67
78
  if not argv:
@@ -141,6 +152,8 @@ def main(argv=None) -> int:
141
152
  return cmd_survey(rest)
142
153
  if cmd == 'guide':
143
154
  return cmd_guide(rest)
155
+ if cmd == 'security':
156
+ return cmd_security(rest)
144
157
  if cmd == 'docs':
145
158
  return cmd_docs(rest)
146
159
  if cmd == 'gui':