Skip to content

Commit 727e8ce

Browse files
authored
Merge pull request #581 from fstagni/securitytxt
feat: added /.well-known/security.txt
2 parents e3d71cb + 564ff57 commit 727e8ce

File tree

6 files changed

+315
-0
lines changed

6 files changed

+315
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Update Security.txt Expiry
2+
3+
on:
4+
schedule:
5+
# Runs once a year
6+
- cron: '15 2 1 1 *' # January 1st at 02:15 UTC
7+
workflow_dispatch: # Allows manual triggering for testing.
8+
9+
jobs:
10+
update-expiry:
11+
runs-on: ubuntu-latest
12+
# Grant GITHUB_TOKEN permissions to create a pull request.
13+
permissions:
14+
contents: write
15+
pull-requests: write
16+
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v4
23+
with:
24+
python-version: '3.x'
25+
26+
- name: Update expiry date in well_known.py
27+
id: update_script
28+
run: |
29+
import re
30+
from datetime import datetime, timedelta, timezone
31+
import os
32+
33+
file_path = "diracx-routers/src/diracx/routers/auth/well_known.py"
34+
changes_made = False
35+
36+
with open(file_path, "r") as f:
37+
content = f.read()
38+
39+
# Using a robust regex to find the line and capture the date
40+
pattern = re.compile(r'''(^\s*Expires: )(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)''', re.MULTILINE)
41+
match = pattern.search(content)
42+
43+
if match:
44+
old_date_str = match.group(2)
45+
try:
46+
expiry_date = datetime.strptime(old_date_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
47+
now = datetime.now(timezone.utc)
48+
49+
# Update if the expiry is less than 45 days away
50+
if (expiry_date - now) < timedelta(days=45):
51+
# Set the new expiry to be 1 year from today
52+
new_expiry_date = now + timedelta(days=365)
53+
new_date_str = new_expiry_date.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
54+
55+
new_content = pattern.sub(r'''\g<1>''' + new_date_str, content)
56+
57+
with open(file_path, "w") as f:
58+
f.write(new_content)
59+
60+
print(f"INFO: Updated expiry date to {new_date_str}")
61+
changes_made = True
62+
else:
63+
print(f"INFO: Expiry date {old_date_str} is not within the update window. No changes made.")
64+
except (ValueError) as e:
65+
print(f"ERROR: Could not parse date string {old_date_str}. Error: {e}")
66+
67+
else:
68+
print(f"ERROR: Could not find the 'Expires:' line in the specified format in {file_path}")
69+
70+
# Set output for subsequent steps
71+
with open(os.environ['GITHUB_OUTPUT'], 'a') as hf:
72+
print(f'changes_made={str(changes_made).lower()}', file=hf)
73+
shell: python
74+
75+
- name: Create Pull Request
76+
if: steps.update_script.outputs.changes_made == 'true'
77+
uses: peter-evans/create-pull-request@v7
78+
with:
79+
token: ${{ secrets.GITHUB_TOKEN }}
80+
commit-message: "chore(security): Update security.txt expiry date"
81+
title: "Automated Security.txt Expiry Update"
82+
body: |
83+
This is an automated PR to update the `Expires` field in the `security.txt` file.
84+
85+
The expiry date is automatically updated to one year from the current date to ensure it remains valid.
86+
branch: "chore/update-security-txt-expiry"
87+
base: "main"
88+
delete-branch: true

diracx-client/src/diracx/client/_generated/aio/operations/_operations.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
build_well_known_get_installation_metadata_request,
5757
build_well_known_get_jwks_request,
5858
build_well_known_get_openid_configuration_request,
59+
build_well_known_get_security_txt_request,
5960
)
6061
from .._configuration import DiracConfiguration
6162

@@ -223,6 +224,53 @@ async def get_installation_metadata(self, **kwargs: Any) -> _models.Metadata:
223224

224225
return deserialized # type: ignore
225226

227+
@distributed_trace_async
228+
async def get_security_txt(self, **kwargs: Any) -> str:
229+
"""Get Security Txt.
230+
231+
Get the security.txt file.
232+
233+
:return: str
234+
:rtype: str
235+
:raises ~azure.core.exceptions.HttpResponseError:
236+
"""
237+
error_map: MutableMapping = {
238+
401: ClientAuthenticationError,
239+
404: ResourceNotFoundError,
240+
409: ResourceExistsError,
241+
304: ResourceNotModifiedError,
242+
}
243+
error_map.update(kwargs.pop("error_map", {}) or {})
244+
245+
_headers = kwargs.pop("headers", {}) or {}
246+
_params = kwargs.pop("params", {}) or {}
247+
248+
cls: ClsType[str] = kwargs.pop("cls", None)
249+
250+
_request = build_well_known_get_security_txt_request(
251+
headers=_headers,
252+
params=_params,
253+
)
254+
_request.url = self._client.format_url(_request.url)
255+
256+
_stream = False
257+
pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access
258+
_request, stream=_stream, **kwargs
259+
)
260+
261+
response = pipeline_response.http_response
262+
263+
if response.status_code not in [200]:
264+
map_error(status_code=response.status_code, response=response, error_map=error_map)
265+
raise HttpResponseError(response=response)
266+
267+
deserialized = self._deserialize("str", pipeline_response.http_response)
268+
269+
if cls:
270+
return cls(pipeline_response, deserialized, {}) # type: ignore
271+
272+
return deserialized # type: ignore
273+
226274

227275
class AuthOperations: # pylint: disable=abstract-class-instantiated
228276
"""

diracx-client/src/diracx/client/_generated/operations/_operations.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,20 @@ def build_well_known_get_installation_metadata_request(**kwargs: Any) -> HttpReq
7777
return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs)
7878

7979

80+
def build_well_known_get_security_txt_request(**kwargs: Any) -> HttpRequest: # pylint: disable=name-too-long
81+
_headers = case_insensitive_dict(kwargs.pop("headers", {}) or {})
82+
83+
accept = _headers.pop("Accept", "application/json")
84+
85+
# Construct URL
86+
_url = "/.well-known/.well-known/security.txt"
87+
88+
# Construct headers
89+
_headers["Accept"] = _SERIALIZER.header("accept", accept, "str")
90+
91+
return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs)
92+
93+
8094
def build_auth_initiate_device_flow_request(*, client_id: str, scope: str, **kwargs: Any) -> HttpRequest:
8195
_headers = case_insensitive_dict(kwargs.pop("headers", {}) or {})
8296
_params = case_insensitive_dict(kwargs.pop("params", {}) or {})
@@ -754,6 +768,53 @@ def get_installation_metadata(self, **kwargs: Any) -> _models.Metadata:
754768

755769
return deserialized # type: ignore
756770

771+
@distributed_trace
772+
def get_security_txt(self, **kwargs: Any) -> str:
773+
"""Get Security Txt.
774+
775+
Get the security.txt file.
776+
777+
:return: str
778+
:rtype: str
779+
:raises ~azure.core.exceptions.HttpResponseError:
780+
"""
781+
error_map: MutableMapping = {
782+
401: ClientAuthenticationError,
783+
404: ResourceNotFoundError,
784+
409: ResourceExistsError,
785+
304: ResourceNotModifiedError,
786+
}
787+
error_map.update(kwargs.pop("error_map", {}) or {})
788+
789+
_headers = kwargs.pop("headers", {}) or {}
790+
_params = kwargs.pop("params", {}) or {}
791+
792+
cls: ClsType[str] = kwargs.pop("cls", None)
793+
794+
_request = build_well_known_get_security_txt_request(
795+
headers=_headers,
796+
params=_params,
797+
)
798+
_request.url = self._client.format_url(_request.url)
799+
800+
_stream = False
801+
pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access
802+
_request, stream=_stream, **kwargs
803+
)
804+
805+
response = pipeline_response.http_response
806+
807+
if response.status_code not in [200]:
808+
map_error(status_code=response.status_code, response=response, error_map=error_map)
809+
raise HttpResponseError(response=response)
810+
811+
deserialized = self._deserialize("str", pipeline_response.http_response)
812+
813+
if cls:
814+
return cls(pipeline_response, deserialized, {}) # type: ignore
815+
816+
return deserialized # type: ignore
817+
757818

758819
class AuthOperations: # pylint: disable=abstract-class-instantiated
759820
"""

diracx-routers/src/diracx/routers/auth/well_known.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,12 @@ async def get_installation_metadata(
5252
) -> Metadata:
5353
"""Get metadata about the dirac installation."""
5454
return await get_installation_metadata_bl(config)
55+
56+
57+
@router.get("/.well-known/security.txt")
58+
async def get_security_txt() -> str:
59+
"""Get the security.txt file."""
60+
return """Contact: https://github.com/DIRACGrid/diracx/security/advisories/new
61+
Expires: 2026-07-02T23:59:59.000Z
62+
Preferred-Languages: en
63+
"""

extensions/gubbins/gubbins-client/src/gubbins/client/_generated/aio/operations/_operations.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
build_well_known_get_installation_metadata_request,
6060
build_well_known_get_jwks_request,
6161
build_well_known_get_openid_configuration_request,
62+
build_well_known_get_security_txt_request,
6263
)
6364
from .._configuration import DiracConfiguration
6465

@@ -179,6 +180,53 @@ async def get_jwks(self, **kwargs: Any) -> Dict[str, Any]:
179180

180181
return deserialized # type: ignore
181182

183+
@distributed_trace_async
184+
async def get_security_txt(self, **kwargs: Any) -> str:
185+
"""Get Security Txt.
186+
187+
Get the security.txt file.
188+
189+
:return: str
190+
:rtype: str
191+
:raises ~azure.core.exceptions.HttpResponseError:
192+
"""
193+
error_map: MutableMapping = {
194+
401: ClientAuthenticationError,
195+
404: ResourceNotFoundError,
196+
409: ResourceExistsError,
197+
304: ResourceNotModifiedError,
198+
}
199+
error_map.update(kwargs.pop("error_map", {}) or {})
200+
201+
_headers = kwargs.pop("headers", {}) or {}
202+
_params = kwargs.pop("params", {}) or {}
203+
204+
cls: ClsType[str] = kwargs.pop("cls", None)
205+
206+
_request = build_well_known_get_security_txt_request(
207+
headers=_headers,
208+
params=_params,
209+
)
210+
_request.url = self._client.format_url(_request.url)
211+
212+
_stream = False
213+
pipeline_response: PipelineResponse = await self._client._pipeline.run( # pylint: disable=protected-access
214+
_request, stream=_stream, **kwargs
215+
)
216+
217+
response = pipeline_response.http_response
218+
219+
if response.status_code not in [200]:
220+
map_error(status_code=response.status_code, response=response, error_map=error_map)
221+
raise HttpResponseError(response=response)
222+
223+
deserialized = self._deserialize("str", pipeline_response.http_response)
224+
225+
if cls:
226+
return cls(pipeline_response, deserialized, {}) # type: ignore
227+
228+
return deserialized # type: ignore
229+
182230
@distributed_trace_async
183231
async def get_installation_metadata(self, **kwargs: Any) -> _models.ExtendedMetadata:
184232
"""Get Installation Metadata.

extensions/gubbins/gubbins-client/src/gubbins/client/_generated/operations/_operations.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,20 @@ def build_well_known_get_jwks_request(**kwargs: Any) -> HttpRequest:
6363
return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs)
6464

6565

66+
def build_well_known_get_security_txt_request(**kwargs: Any) -> HttpRequest: # pylint: disable=name-too-long
67+
_headers = case_insensitive_dict(kwargs.pop("headers", {}) or {})
68+
69+
accept = _headers.pop("Accept", "application/json")
70+
71+
# Construct URL
72+
_url = "/.well-known/.well-known/security.txt"
73+
74+
# Construct headers
75+
_headers["Accept"] = _SERIALIZER.header("accept", accept, "str")
76+
77+
return HttpRequest(method="GET", url=_url, headers=_headers, **kwargs)
78+
79+
6680
def build_well_known_get_installation_metadata_request(**kwargs: Any) -> HttpRequest: # pylint: disable=name-too-long
6781
_headers = case_insensitive_dict(kwargs.pop("headers", {}) or {})
6882

@@ -756,6 +770,53 @@ def get_jwks(self, **kwargs: Any) -> Dict[str, Any]:
756770

757771
return deserialized # type: ignore
758772

773+
@distributed_trace
774+
def get_security_txt(self, **kwargs: Any) -> str:
775+
"""Get Security Txt.
776+
777+
Get the security.txt file.
778+
779+
:return: str
780+
:rtype: str
781+
:raises ~azure.core.exceptions.HttpResponseError:
782+
"""
783+
error_map: MutableMapping = {
784+
401: ClientAuthenticationError,
785+
404: ResourceNotFoundError,
786+
409: ResourceExistsError,
787+
304: ResourceNotModifiedError,
788+
}
789+
error_map.update(kwargs.pop("error_map", {}) or {})
790+
791+
_headers = kwargs.pop("headers", {}) or {}
792+
_params = kwargs.pop("params", {}) or {}
793+
794+
cls: ClsType[str] = kwargs.pop("cls", None)
795+
796+
_request = build_well_known_get_security_txt_request(
797+
headers=_headers,
798+
params=_params,
799+
)
800+
_request.url = self._client.format_url(_request.url)
801+
802+
_stream = False
803+
pipeline_response: PipelineResponse = self._client._pipeline.run( # pylint: disable=protected-access
804+
_request, stream=_stream, **kwargs
805+
)
806+
807+
response = pipeline_response.http_response
808+
809+
if response.status_code not in [200]:
810+
map_error(status_code=response.status_code, response=response, error_map=error_map)
811+
raise HttpResponseError(response=response)
812+
813+
deserialized = self._deserialize("str", pipeline_response.http_response)
814+
815+
if cls:
816+
return cls(pipeline_response, deserialized, {}) # type: ignore
817+
818+
return deserialized # type: ignore
819+
759820
@distributed_trace
760821
def get_installation_metadata(self, **kwargs: Any) -> _models.ExtendedMetadata:
761822
"""Get Installation Metadata.

0 commit comments

Comments
 (0)