@@ -431,6 +431,103 @@ def replace_th_attr(self, content):
431431 content = re .sub (r'th:attr="[^"]*"' , "" , content )
432432 return content
433433
434+ def add_static_assets_challenge58 (self , content ):
435+ """Add embedded CSS and JS for Challenge 58 static preview."""
436+ if "<head>" in content :
437+ head_additions = f"""
438+ <meta charset="UTF-8">
439+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
440+ <title>OWASP WrongSecrets - Challenge 58 Preview</title>
441+ <style>
442+ { self .embedded_css }
443+ .preview-banner {{
444+ background: #f8f9fa;
445+ border: 1px solid #dee2e6;
446+ padding: 10px 15px;
447+ margin-bottom: 20px;
448+ border-radius: 5px;
449+ }}
450+ .preview-banner .alert-heading {{
451+ color: #0c5460;
452+ font-size: 1.1em;
453+ margin-bottom: 5px;
454+ }}
455+ .solved {{ background-color: #d4edda; }}
456+
457+ /* Challenge 58 specific styles */
458+ .demo-section {{
459+ background: #fff3cd;
460+ border: 1px solid #ffeaa7;
461+ border-radius: 6px;
462+ padding: 15px;
463+ margin: 15px 0;
464+ }}
465+
466+ .demo-section .btn-warning {{
467+ background-color: #ffc107;
468+ border-color: #ffc107;
469+ color: #212529;
470+ text-decoration: none;
471+ display: inline-block;
472+ padding: 8px 16px;
473+ border-radius: 4px;
474+ border: 1px solid transparent;
475+ font-weight: 400;
476+ text-align: center;
477+ vertical-align: middle;
478+ cursor: pointer;
479+ font-size: 1rem;
480+ line-height: 1.5;
481+ margin-top: 10px;
482+ }}
483+
484+ .demo-section .btn-warning:hover {{
485+ background-color: #e0a800;
486+ border-color: #d39e00;
487+ }}
488+
489+ /* Challenge explanation sections */
490+ .challenge-content {{
491+ margin-bottom: 30px;
492+ }}
493+ .explanation-content, .hint-content, .reason-content {{
494+ background: #f8f9fa;
495+ border: 1px solid #e9ecef;
496+ border-radius: 6px;
497+ padding: 15px;
498+ margin-bottom: 20px;
499+ }}
500+ .explanation-content h3, .hint-content h3, .reason-content h3 {{
501+ color: #495057;
502+ margin-top: 0;
503+ }}
504+ .explanation-content ul, .hint-content ul, .reason-content ul {{
505+ margin-bottom: 10px;
506+ }}
507+ .explanation-content li, .hint-content li, .reason-content li {{
508+ margin-bottom: 5px;
509+ }}
510+ </style>"""
511+ content = content .replace ("<head>" , f"<head>{ head_additions } " )
512+
513+ # Add preview banner for Challenge 58
514+ banner = f"""
515+ <div class="preview-banner">
516+ <div class="alert-heading">🗄️ Challenge 58 - Database Connection String Exposure (PR #{ self .pr_number } )</div>
517+ <small>This is a live preview of Challenge 58 demonstrating how database credentials leak through error messages. Click the demo button to see the vulnerable endpoint in action!</small>
518+ </div>"""
519+
520+ if '<div class="container"' in content :
521+ content = content .replace (
522+ '<div class="container"' , f'<div class="container">{ banner } '
523+ )
524+ elif "<body>" in content :
525+ content = content .replace (
526+ "<body>" , f'<body><div class="container">{ banner } </div>'
527+ )
528+
529+ return content
530+
434531 def add_static_assets (self , content , template_name ):
435532 """Add embedded CSS and JS for the static preview."""
436533 if "<head>" in content :
@@ -632,6 +729,120 @@ def generate_stats_page(self):
632729
633730 return content
634731
732+ def generate_challenge58_page (self ):
733+ """Generate Challenge 58 (Database Connection String Exposure) page with embedded content."""
734+ template_path = self .templates_dir / "challenge.html"
735+
736+ if not template_path .exists ():
737+ print (f"Warning: Template { template_path } not found" )
738+ return self .generate_fallback_challenge58 ()
739+
740+ with open (template_path , "r" , encoding = "utf-8" ) as f :
741+ content = f .read ()
742+
743+ # Mock Challenge 58 data
744+ mock_challenge = {
745+ "name" : "Challenge 58: Database Connection String Exposure" ,
746+ "stars" : "⭐⭐⭐" ,
747+ "tech" : "LOGGING" ,
748+ "explanation" : "challenge58.adoc" ,
749+ "hint" : "challenge58_hint.adoc" ,
750+ "reason" : "challenge58_reason.adoc" ,
751+ "link" : "/challenge/challenge-58" ,
752+ }
753+
754+ # Replace challenge-specific Thymeleaf content
755+ content = re .sub (
756+ r'<span th:text="\$\{challenge\.name\}"[^>]*>[^<]*</span>' ,
757+ f'<span data-cy="challenge-title">{ mock_challenge ["name" ]} </span>' ,
758+ content ,
759+ )
760+ content = re .sub (
761+ r'<span[^>]*th:text="\$\{challenge\.stars\}"[^>]*>[^<]*</span>' ,
762+ f'<span>{ mock_challenge ["stars" ]} </span>' ,
763+ content ,
764+ )
765+ content = re .sub (
766+ r'<strong th:text="\$\{challenge\.tech\}"[^>]*>[^<]*</strong>' ,
767+ f'<strong>{ mock_challenge ["tech" ]} </strong>' ,
768+ content ,
769+ )
770+ content = re .sub (
771+ r'<span th:text="\'Welcome to challenge \'\s*\+\s*\$\{challenge\.name\}\s*\+\s*\'\.\'"></span>' ,
772+ f'<span>Welcome to challenge { mock_challenge ["name" ]} .</span>' ,
773+ content ,
774+ )
775+
776+ # Replace the explanation section with Challenge 58 content
777+ explanation_pattern = (
778+ r'<div th:replace="~\{doc:__\$\{challenge\.explanation\}__\}"></div>'
779+ )
780+
781+ # Load actual Challenge 58 content from AsciiDoc files
782+ explanation_content = self .load_adoc_content ("challenge58.adoc" )
783+ hint_content = self .load_adoc_content ("challenge58_hint.adoc" )
784+ reason_content = self .load_adoc_content ("challenge58_reason.adoc" )
785+
786+ challenge58_explanation = f"""
787+ <div class="challenge-explanation">
788+ <div class="challenge-content">
789+ <h4>📖 Challenge Explanation</h4>
790+ <div class="explanation-content">
791+ { explanation_content }
792+ </div>
793+
794+ <h4>💡 Hints</h4>
795+ <div class="hint-content">
796+ { hint_content }
797+ </div>
798+
799+ <h4>🧠 Reasoning</h4>
800+ <div class="reason-content">
801+ { reason_content }
802+ </div>
803+ </div>
804+
805+ <div class="challenge-demo">
806+ <h4>🔗 Database Connection Error Demo</h4>
807+ <div class="demo-section">
808+ <p><strong>Try the vulnerable endpoint:</strong></p>
809+ <a href="/error-demo/database-connection" class="btn btn-warning">
810+ 🚨 Trigger Database Connection Error
811+ </a>
812+ <p><small class="text-muted">This endpoint simulates a database connection failure that exposes the connection string with embedded credentials.</small></p>
813+ </div>
814+ </div>
815+ </div>
816+ """
817+ content = re .sub (
818+ explanation_pattern , lambda m : challenge58_explanation , content
819+ )
820+
821+ # Process the template
822+ content = self .process_thymeleaf_syntax (content , "challenge58" )
823+
824+ # Ensure we have a proper HTML structure with head
825+ if "<head>" not in content :
826+ # Add basic HTML structure
827+ content = f"""<!DOCTYPE html>
828+ <html lang="en">
829+ <head>
830+ <meta charset="UTF-8">
831+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
832+ <title>OWASP WrongSecrets - Challenge 58</title>
833+ </head>
834+ { content }
835+ </html>"""
836+
837+ # Add embedded CSS and styling for Challenge 58
838+ content = self .add_static_assets_challenge58 (content )
839+
840+ # Add navigation
841+ nav = self .generate_navigation_html ()
842+ content = content .replace ("<body>" , f"<body>{ nav } " )
843+
844+ return content
845+
635846 def generate_challenge57_page (self ):
636847 """Generate Challenge 57 (LLM Challenge) page with embedded content."""
637848 template_path = self .templates_dir / "challenge.html"
@@ -899,6 +1110,57 @@ def generate_fallback_challenge57_snippet(self):
8991110 </script>
9001111 """
9011112
1113+ def generate_fallback_challenge58 (self ):
1114+ """Generate a fallback Challenge 58 page if template is missing."""
1115+ return f"""<!DOCTYPE html>
1116+ <html lang="en">
1117+ <head>
1118+ <meta charset="UTF-8">
1119+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1120+ <title>OWASP WrongSecrets - Challenge 58</title>
1121+ <style>
1122+ { self .embedded_css }
1123+ </style>
1124+ </head>
1125+ <body>
1126+ { self .generate_navigation_html ()}
1127+ <div class="container mt-4">
1128+ <div class="preview-banner">
1129+ <div class="alert-heading">🗄️ Challenge 58 - Database Connection String Exposure (PR #{ self .pr_number } )</div>
1130+ <small>This is a live preview of Challenge 58 demonstrating how database credentials leak through error messages.</small>
1131+ </div>
1132+
1133+ <h1>Challenge 58: Database Connection String Exposure ⭐⭐⭐</h1>
1134+ <p>Welcome to Challenge 58: Database Connection String Exposure.</p>
1135+
1136+ <div class="alert alert-primary" role="alert">
1137+ <h6 class="alert-heading">🔍 Your Task</h6>
1138+ <p class="mb-2">Find the database password that gets exposed when the application fails to connect to the database.</p>
1139+ <p class="mb-0">💡 <strong>Visit:</strong> The <code>/error-demo/database-connection</code> endpoint to trigger the error.</p>
1140+ </div>
1141+
1142+ <div class="demo-section">
1143+ <h4>🔗 Database Connection Error Demo</h4>
1144+ <p><strong>Try the vulnerable endpoint:</strong></p>
1145+ <a href="/error-demo/database-connection" class="btn btn-warning">
1146+ 🚨 Trigger Database Connection Error
1147+ </a>
1148+ <p><small class="text-muted">This endpoint simulates a database connection failure that exposes the connection string with embedded credentials.</small></p>
1149+ </div>
1150+
1151+ <form>
1152+ <div class="mb-3">
1153+ <label for="answerfield" class="form-label"><strong>🔑 Enter the database password you found:</strong></label>
1154+ <input type="text" class="form-control" id="answerfield" placeholder="Type the password here..."/>
1155+ <small class="form-text text-muted">💡 Tip: Look for the password in the database connection error message.</small>
1156+ </div>
1157+ <button class="btn btn-primary" type="button">🚀 Submit Answer</button>
1158+ <button class="btn btn-secondary" type="button" onclick="document.getElementById('answerfield').value='';">🗑️ Clear</button>
1159+ </form>
1160+ </div>
1161+ </body>
1162+ </html>"""
1163+
9021164 def generate_fallback_challenge57 (self ):
9031165 """Generate a fallback Challenge 57 page if template is missing."""
9041166 return f"""<!DOCTYPE html>
@@ -1069,7 +1331,7 @@ def generate_fallback_challenge(self):
10691331</html>"""
10701332
10711333 def generate_all_pages (self ):
1072- """Generate all static pages with Challenge 57 as the featured challenge."""
1334+ """Generate all static pages with Challenge 58 as the featured latest challenge."""
10731335 # Create pages directory
10741336 pages_dir = self .static_dir / f"pr-{ self .pr_number } " / "pages"
10751337 pages_dir .mkdir (parents = True , exist_ok = True )
@@ -1078,8 +1340,9 @@ def generate_all_pages(self):
10781340 "welcome.html" : self .generate_welcome_page (),
10791341 "about.html" : self .generate_about_page (),
10801342 "stats.html" : self .generate_stats_page (),
1081- "challenge-57.html" : self .generate_challenge57_page (), # Always render Challenge 57
1082- "challenge-example.html" : self .generate_challenge57_page (), # Use Challenge 57 as the example too
1343+ "challenge-57.html" : self .generate_challenge57_page (), # LLM Challenge (AI category)
1344+ "challenge-58.html" : self .generate_challenge58_page (), # Database Challenge (Latest)
1345+ "challenge-example.html" : self .generate_challenge58_page (), # Use Challenge 58 as the latest example
10831346 }
10841347
10851348 for filename , content in pages .items ():
@@ -1089,7 +1352,10 @@ def generate_all_pages(self):
10891352 print (f"Generated { filename } " )
10901353
10911354 print (f"Generated { len (pages )} static pages in { pages_dir } " )
1092- print (f"✅ Challenge 57 (LLM Security) is featured as the latest challenge" )
1355+ print (
1356+ f"✅ Challenge 57 (LLM Security) and Challenge 58 (Database Connection String Exposure) are both available"
1357+ )
1358+ print (f"✅ Challenge 58 is featured as the latest challenge" )
10931359 return pages_dir
10941360
10951361
0 commit comments